React
Radio Group
여러 옵션 중 하나를 선택할 수 있도록 할 때 사용하는 컴포넌트입니다.
import { VStack } from "@seed-design/react";
import { RadioGroup, RadioGroupItem } from "seed-design/ui/radio-group";
export default function RadioGroupPreview() {
return (
<VStack p="x6">
<RadioGroup
defaultValue="apple"
label="좋아하는 과일"
description="좋아하는 과일을 선택해 주세요."
indicator="선택"
>
<RadioGroupItem value="apple" label="Apple" tone="neutral" size="large" />
<RadioGroupItem value="banana" label="Banana" tone="neutral" size="large" />
<RadioGroupItem value="orange" label="Orange" tone="neutral" size="large" />
</RadioGroup>
</VStack>
);
}Installation
npx @seed-design/cli@latest add ui:radio-grouppnpm dlx @seed-design/cli@latest add ui:radio-groupyarn dlx @seed-design/cli@latest add ui:radio-groupbun x @seed-design/cli@latest add ui:radio-group의존성 설치
npm install @karrotmarket/react-monochrome-icon @seed-design/css @seed-design/reactyarn add @karrotmarket/react-monochrome-icon @seed-design/css @seed-design/reactpnpm add @karrotmarket/react-monochrome-icon @seed-design/css @seed-design/reactbun add @karrotmarket/react-monochrome-icon @seed-design/css @seed-design/react아래 코드를 복사 후 붙여넣고 사용하세요
/**
* @file ui:radio-group
* @requires @seed-design/react@^2.0.0
* @requires @seed-design/css@^2.0.0
**/
"use client";
import { IconExclamationmarkCircleFill } from "@karrotmarket/react-monochrome-icon";
import {
RadioGroup as SeedRadioGroup,
RadioGroupField as SeedRadioGroupField,
PrefixIcon,
VisuallyHidden,
} from "@seed-design/react";
import type { FieldLabelVariantProps } from "@seed-design/css/recipes/field-label";
import { radioGroup, type RadioGroupVariantProps } from "@seed-design/css/recipes/radio-group";
import * as React from "react";
export interface RadioGroupProps extends SeedRadioGroupField.RootProps, RadioGroupVariantProps {
label?: React.ReactNode;
/**
* @default "medium"
*/
labelWeight?: FieldLabelVariantProps["weight"];
indicator?: React.ReactNode;
showRequiredIndicator?: boolean;
description?: React.ReactNode;
errorMessage?: React.ReactNode;
}
/**
* @see https://seed-design.io/react/components/radio-group
*/
export const RadioGroup = React.forwardRef<HTMLDivElement, RadioGroupProps>(
(
{
label,
labelWeight,
indicator,
showRequiredIndicator,
description,
errorMessage,
children,
...props
},
ref,
) => {
const [variantProps, restProps] = radioGroup.splitVariantProps(props);
if (
process.env.NODE_ENV !== "production" &&
!label &&
!restProps["aria-label"] &&
!restProps["aria-labelledby"]
) {
console.warn(
"RadioGroup component requires a `label`, `aria-label` or `aria-labelledby` attribute.",
);
}
const renderErrorMessage = errorMessage && restProps.invalid;
const renderFooter = description || renderErrorMessage;
return (
<SeedRadioGroupField.Root ref={ref} {...restProps}>
{(label || indicator) && (
<SeedRadioGroupField.Header>
<SeedRadioGroupField.Label weight={labelWeight}>
{label}
{showRequiredIndicator && <SeedRadioGroupField.RequiredIndicator />}
{indicator && (
<SeedRadioGroupField.IndicatorText>{indicator}</SeedRadioGroupField.IndicatorText>
)}
</SeedRadioGroupField.Label>
</SeedRadioGroupField.Header>
)}
<SeedRadioGroup.Root {...variantProps}>{children}</SeedRadioGroup.Root>
{renderFooter && (
<SeedRadioGroupField.Footer>
{description &&
(renderErrorMessage ? (
<VisuallyHidden asChild>
<SeedRadioGroupField.Description>{description}</SeedRadioGroupField.Description>
</VisuallyHidden>
) : (
<SeedRadioGroupField.Description>{description}</SeedRadioGroupField.Description>
))}
{renderErrorMessage && (
<SeedRadioGroupField.ErrorMessage>
<PrefixIcon svg={<IconExclamationmarkCircleFill />} />
{errorMessage}
</SeedRadioGroupField.ErrorMessage>
)}
</SeedRadioGroupField.Footer>
)}
</SeedRadioGroupField.Root>
);
},
);
RadioGroup.displayName = "RadioGroup";
export interface RadioGroupItemProps extends SeedRadioGroup.ItemProps {
label?: React.ReactNode;
inputProps?: React.InputHTMLAttributes<HTMLInputElement>;
rootRef?: React.Ref<HTMLLabelElement>;
}
/**
* @see https://seed-design.io/react/components/radio-group
*/
export const RadioGroupItem = React.forwardRef<HTMLInputElement, RadioGroupItemProps>(
({ label, inputProps, rootRef, ...otherProps }, ref) => {
return (
<SeedRadioGroup.Item ref={rootRef} {...otherProps}>
<SeedRadioGroup.ItemControl>
<SeedRadioGroup.ItemIndicator
checked={
<svg aria-hidden="true" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="12" fill="currentColor" />
</svg>
}
/>
</SeedRadioGroup.ItemControl>
{label && <SeedRadioGroup.ItemLabel>{label}</SeedRadioGroup.ItemLabel>}
<SeedRadioGroup.ItemHiddenInput ref={ref} {...inputProps} />
</SeedRadioGroup.Item>
);
},
);
RadioGroupItem.displayName = "RadioGroupItem";
export interface RadiomarkProps extends SeedRadioGroup.ItemControlProps {}
/**
* @see https://seed-design.io/react/components/radio-group
*/
export const Radiomark = React.forwardRef<HTMLDivElement, RadiomarkProps>((props, ref) => {
return (
<SeedRadioGroup.ItemControl ref={ref} {...props}>
<SeedRadioGroup.ItemIndicator
checked={
<svg aria-hidden="true" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="12" fill="currentColor" />
</svg>
}
/>
</SeedRadioGroup.ItemControl>
);
});
Radiomark.displayName = "Radiomark";
/**
* @deprecated Use `Radiomark` instead. Will be removed in @seed-design/[email protected].
*/
export const RadioMark = Radiomark;
/**
* @deprecated Use `RadiomarkProps` instead. Will be removed in @seed-design/[email protected].
*/
export type RadioMarkProps = RadiomarkProps;
/**
* This file is a snippet from SEED Design, helping you get started quickly with @seed-design/* packages.
* You can extend this snippet however you want.
*/
Props
RadioGroup
Prop
Type
label?React.ReactNodeindicator?React.ReactNodeshowRequiredIndicator?boolean | undefineddescription?React.ReactNodeerrorMessage?React.ReactNodename?string | undefinedform?string | undefinedvalue?string | undefineddefaultValue?string | undefinedonValueChange?((value: string) => void) | undefinedRadioGroupItem
Prop
Type
label?React.ReactNodeinputProps?React.InputHTMLAttributes<HTMLInputElement> | undefinedrootRef?React.Ref<HTMLLabelElement> | undefinedvaluestringdisabled?boolean | undefinedRadiomark
Prop
Type
Examples
Sizes
import { RadioGroup, RadioGroupItem } from "seed-design/ui/radio-group";
import { VStack } from "@seed-design/react";
export default function RadioGroupSize() {
return (
<VStack gap="x5" p="x6">
<RadioGroup defaultValue="apple" aria-label="과일 선택">
<RadioGroupItem value="apple" label="사과" size="medium" tone="neutral" />
<RadioGroupItem value="banana" label="바나나" size="medium" tone="neutral" />
<RadioGroupItem value="orange" label="오렌지" size="medium" tone="neutral" />
</RadioGroup>
<RadioGroup defaultValue="red" aria-label="색상 선택">
<RadioGroupItem value="red" label="빨간색" size="large" tone="neutral" />
<RadioGroupItem value="blue" label="파란색" size="large" tone="neutral" />
<RadioGroupItem value="green" label="초록색" size="large" tone="neutral" />
</RadioGroup>
</VStack>
);
}Tones
Brand
import { VStack } from "@seed-design/react";
import { RadioGroup, RadioGroupItem } from "seed-design/ui/radio-group";
export default function RadioGroupBrand() {
return (
<VStack p="x6">
<RadioGroup defaultValue="apple" aria-label="과일 선택">
<RadioGroupItem value="apple" label="사과" tone="brand" size="large" />
<RadioGroupItem value="banana" label="바나나" tone="brand" size="large" />
<RadioGroupItem value="orange" label="오렌지" tone="brand" size="large" />
</RadioGroup>
</VStack>
);
}Neutral
import { VStack } from "@seed-design/react";
import { RadioGroup, RadioGroupItem } from "seed-design/ui/radio-group";
export default function RadioGroupNeutral() {
return (
<VStack p="x6">
<RadioGroup defaultValue="apple" aria-label="과일 선택">
<RadioGroupItem value="apple" label="사과" tone="neutral" size="large" />
<RadioGroupItem value="banana" label="바나나" tone="neutral" size="large" />
<RadioGroupItem value="orange" label="오렌지" tone="neutral" size="large" />
</RadioGroup>
</VStack>
);
}Weights
import { VStack } from "@seed-design/react";
import { RadioGroup, RadioGroupItem } from "seed-design/ui/radio-group";
export default function RadioGroupWeights() {
return (
<VStack p="x6">
<RadioGroup defaultValue="regular" aria-label="글꼴 굵기 선택">
<RadioGroupItem
value="regular"
label="Regular"
weight="regular"
tone="neutral"
size="large"
/>
<RadioGroupItem value="bold" label="Bold" weight="bold" tone="neutral" size="large" />
</RadioGroup>
</VStack>
);
}Long Label
import { VStack } from "@seed-design/react";
import { RadioGroup, RadioGroupItem } from "seed-design/ui/radio-group";
export default function RadioGroupLongLabel() {
return (
<VStack p="x6">
<RadioGroup defaultValue="medium" aria-label="Long label options">
<RadioGroupItem
value="medium"
size="medium"
tone="neutral"
label="Consequat ut veniam aliqua deserunt occaecat enim occaecat veniam et et cillum nulla officia incididunt incididunt. Sint laboris labore occaecat fugiat culpa voluptate ullamco in elit dolore exercitation nulla."
/>
<RadioGroupItem
value="large"
size="large"
tone="neutral"
label="Consequat ut veniam aliqua deserunt occaecat enim occaecat veniam et et cillum nulla officia incididunt incididunt. Sint laboris labore occaecat fugiat culpa voluptate ullamco in elit dolore exercitation nulla."
/>
</RadioGroup>
</VStack>
);
}Disabled
import { VStack } from "@seed-design/react";
import { RadioGroup, RadioGroupItem } from "seed-design/ui/radio-group";
export default function RadioGroupDisabled() {
return (
<VStack p="x6">
<RadioGroup defaultValue="option1" aria-label="Options with disabled">
<RadioGroupItem value="option1" label="Active option" tone="neutral" size="large" />
<RadioGroupItem
value="option2"
label="Disabled option"
tone="neutral"
size="large"
disabled
/>
<RadioGroupItem value="option3" label="Another active option" tone="neutral" size="large" />
</RadioGroup>
</VStack>
);
}Listening to Value Changes
RadioGroup의 onValueChange를 사용하여 라디오 버튼의 선택 값 변경을 감지할 수 있습니다.
이벤트를 활용해야 하는 경우 RadioGroupItem의 inputProps를 통해 내부 <input> 요소에 직접 이벤트 핸들러를 추가할 수 있습니다.
import { VStack, Text } from "@seed-design/react";
import { RadioGroup, RadioGroupItem } from "seed-design/ui/radio-group";
import { useState } from "react";
export default function RadioGroupValueChanges() {
const [count, setCount] = useState(0);
const [lastValue, setLastValue] = useState<string | null>(null);
return (
<VStack gap="x4" align="center" width="full" p="x6">
<RadioGroup
defaultValue="apple"
aria-label="Fruit selection"
onValueChange={(value) => {
setCount((prev) => prev + 1);
setLastValue(value);
}}
>
<RadioGroupItem value="apple" label="Apple" tone="neutral" size="large" />
<RadioGroupItem value="banana" label="Banana" tone="neutral" size="large" />
<RadioGroupItem value="orange" label="Orange" tone="neutral" size="large" />
</RadioGroup>
<Text>
onValueChange called: {count} times, last value: {lastValue ?? "-"}
</Text>
</VStack>
);
}Use Cases
React Hook Form
import { HStack, VStack } from "@seed-design/react";
import { useCallback, type FormEvent } from "react";
import { useController, useForm } from "react-hook-form";
import { ActionButton } from "seed-design/ui/action-button";
import { RadioGroup, RadioGroupItem } from "seed-design/ui/radio-group";
const POSSIBLE_COLORS = ["red", "blue", "green"] as const;
interface FormValues {
color: (typeof POSSIBLE_COLORS)[number];
}
export default function RadioGroupReactHookForm() {
const { handleSubmit, reset, setValue, control } = useForm<FormValues>({
defaultValues: {
color: "blue",
},
});
const { field } = useController({ name: "color", control });
const onValid = useCallback((data: FormValues) => {
window.alert(JSON.stringify(data, null, 2));
}, []);
const onReset = useCallback(
(event: FormEvent) => {
event.preventDefault();
reset();
},
[reset],
);
return (
<VStack p="x6" as="form" onSubmit={handleSubmit(onValid)}>
<VStack gap="x3">
<RadioGroup
value={field.value}
onValueChange={(value) => setValue("color", value as FormValues["color"])}
aria-label="Color selection"
>
{POSSIBLE_COLORS.map((color) => (
<RadioGroupItem
key={color}
value={color}
label={color.charAt(0).toUpperCase() + color.slice(1)}
tone="neutral"
size="large"
/>
))}
</RadioGroup>
<HStack gap="x3">
<ActionButton type="submit" variant="neutralSolid">
Submit
</ActionButton>
<ActionButton variant="neutralWeak" onClick={onReset}>
Reset
</ActionButton>
</HStack>
</VStack>
</VStack>
);
}Using Radiomark
Radiomark는 독립적인 라디오 마크 컴포넌트로, Radio Group Primitive 컴포넌트와 조합하여 커스텀 레이아웃을 위해 사용할 수 있습니다.
import { HStack, Text, VStack } from "@seed-design/react";
import { Radiomark } from "seed-design/ui/radio-group";
import { RadioGroup } from "@seed-design/react/primitive";
function CustomRadioGroupItem({ children, ...props }: RadioGroup.ItemProps) {
return (
<VStack asChild gap="x2" align="center">
<RadioGroup.Item {...props}>
<Radiomark tone="neutral" />
<RadioGroup.ItemHiddenInput />
{children}
</RadioGroup.Item>
</VStack>
);
}
export default function RadioGroupRadiomark() {
return (
<VStack p="x6">
<RadioGroup.Root defaultValue="medium" aria-label="Weight selection">
<HStack gap="x6">
<CustomRadioGroupItem value="regular">
<Text textStyle="t7Regular">regular</Text>
</CustomRadioGroupItem>
<CustomRadioGroupItem value="medium">
<Text textStyle="t7Medium">medium</Text>
</CustomRadioGroupItem>
<CustomRadioGroupItem value="bold">
<Text textStyle="t7Bold">bold</Text>
</CustomRadioGroupItem>
</HStack>
</RadioGroup.Root>
</VStack>
);
}RadioGroupField Integration
label, description, errorMessage 등의 RadioGroupField 관련 prop을 사용할 수 있습니다.
import { ActionButton, HStack, VStack } from "@seed-design/react";
import { useState } from "react";
import { RadioGroup, RadioGroupItem } from "seed-design/ui/radio-group";
export default function RadioGroupRadioGroupField() {
const [firstErrorMessage, setFirstErrorMessage] = useState<string | undefined>();
const [secondErrorMessage, setSecondErrorMessage] = useState<string | undefined>();
const handleFirstSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const contact = formData.get("contact");
if (contact === "email") {
setFirstErrorMessage("이메일은 선택할 수 없습니다.");
return;
}
setFirstErrorMessage(undefined);
alert(JSON.stringify({ contact }, null, 2));
};
const handleSecondSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const option = formData.get("option");
if (option === "option1") {
setSecondErrorMessage("옵션 1은 선택할 수 없습니다.");
return;
}
setSecondErrorMessage(undefined);
alert(JSON.stringify({ option }, null, 2));
};
return (
<HStack width="full" gap="x8" align="flex-start" p="x6">
<VStack asChild gap="spacingY.componentDefault" style={{ flex: 1 }}>
<form onSubmit={handleFirstSubmit}>
<RadioGroup
label="선호하는 연락 방법"
indicator="필수"
description="이메일을 선택하고 제출해보세요."
name="contact"
defaultValue="email"
invalid={!!firstErrorMessage}
errorMessage={firstErrorMessage}
>
<RadioGroupItem value="email" label="이메일" tone="neutral" size="large" />
<RadioGroupItem value="phone" label="전화" tone="neutral" size="large" />
<RadioGroupItem value="sms" label="문자" tone="neutral" size="large" />
</RadioGroup>
<ActionButton type="submit" variant="neutralSolid">
제출
</ActionButton>
</form>
</VStack>
<VStack asChild gap="spacingY.componentDefault" style={{ flex: 1 }}>
<form onSubmit={handleSecondSubmit}>
<RadioGroup
label="필수 선택"
labelWeight="bold"
showRequiredIndicator
description="옵션 1을 선택하고 제출해보세요."
name="option"
defaultValue="option1"
invalid={!!secondErrorMessage}
errorMessage={secondErrorMessage}
>
<RadioGroupItem value="option1" label="옵션 1" tone="neutral" size="large" />
<RadioGroupItem value="option2" label="옵션 2" tone="neutral" size="large" disabled />
<RadioGroupItem value="option3" label="옵션 3" tone="neutral" size="large" />
</RadioGroup>
<ActionButton type="submit" variant="neutralSolid">
제출
</ActionButton>
</form>
</VStack>
</HStack>
);
}Last updated on