사용자가 여러 옵션 중 하나만 선택할 수 있게 하는 라디오 그룹 컴포넌트입니다.
@seed-design/lynx-react@0.1.0, @seed-design/lynx-css@0.1.0
import "./styles" ;
import { VStack, useSeedClassName } from "@seed-design/lynx-react" ;
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" ;
export default function Example () {
const seedClassName = useSeedClassName ({ colorMode: "system" });
return (
< view className = { `${ seedClassName } docs-lynx-radio-group-root` }>
< VStack className = "radio-group-preview" >
< RadioGroup
defaultValue = "apple"
label = "좋아하는 과일"
description = "좋아하는 과일을 선택해 주세요."
indicator = "선택"
tone = "neutral"
size = "large"
>
< RadioGroupItem value = "apple" label = "Apple" />
< RadioGroupItem value = "banana" label = "Banana" />
< RadioGroupItem value = "orange" label = "Orange" />
</ RadioGroup >
</ VStack >
</ view >
);
}
npx @seed-design/cli add ui:radio-group pnpm dlx @seed-design/cli add ui:radio-group yarn dlx @seed-design/cli add ui:radio-group bun x @seed-design/cli add ui:radio-group
의존성 설치
npm install @seed-design/lynx-react yarn add @seed-design/lynx-react pnpm add @seed-design/lynx-react bun add @seed-design/lynx-react 아래 코드를 복사 후 붙여넣고 사용하세요 /**
* @file ui:radio-group
* @requires @seed-design/lynx-react@>=0.4.0 <1.0.0
* @requires @seed-design/lynx-css@>=0.8.0 <1.0.0
**/
import * as React from "@lynx-js/react" ;
import { Field as SeedField, RadioGroup as SeedRadioGroup } from "@seed-design/lynx-react" ;
type FieldRootRef = React . ComponentRef < typeof SeedField.Root>;
export interface RadioGroupProps
extends SeedRadioGroup . RootProps ,
Pick < SeedField . RootProps , "required" | "invalid" | "readOnly" > {
label ?: React . ReactNode ;
/**
* @default " medium "
*/
labelWeight ?: SeedField . LabelProps [ "weight" ];
indicator ?: React . ReactNode ;
showRequiredIndicator ?: boolean ;
description ?: React . ReactNode ;
errorMessage ?: React . ReactNode ;
}
/**
* @see https://seed-design.io/lynx/components/radio-group
*/
export const RadioGroup = React. forwardRef < FieldRootRef , RadioGroupProps >(
(
{
label,
labelWeight,
indicator,
showRequiredIndicator,
description,
errorMessage,
children,
"accessibility-label" : accessibilityLabel,
value,
defaultValue,
onValueChange,
disabled,
required,
invalid,
readOnly,
weight,
size,
tone,
... fieldProps
},
ref,
) => {
const renderHeader = label != null || indicator != null ;
const renderErrorMessage = invalid && errorMessage != null ;
const renderDescription = description != null && ! renderErrorMessage;
const renderFooter = renderDescription || renderErrorMessage;
const defaultAccessibilityLabel = typeof label === "string" ? label : undefined ;
return (
< SeedField.Root
ref = {ref}
required = {required}
disabled = {disabled}
invalid = {invalid}
readOnly = {readOnly}
{ ... fieldProps}
>
{renderHeader ? (
< SeedField.Header >
< SeedField.Label weight = {labelWeight}>
{label}
{showRequiredIndicator ? < SeedField.RequiredIndicator /> : null }
{indicator != null ? (
< SeedField.IndicatorText >{indicator}</ SeedField.IndicatorText >
) : null }
</ SeedField.Label >
</ SeedField.Header >
) : null }
< SeedRadioGroup.Root
accessibility-label = {accessibilityLabel ?? defaultAccessibilityLabel}
value = {value}
defaultValue = {defaultValue}
onValueChange = {onValueChange}
disabled = {disabled}
weight = {weight}
size = {size}
tone = {tone}
>
{children}
</ SeedRadioGroup.Root >
{renderFooter ? (
< SeedField.Footer >
{renderDescription ? (
< SeedField.Description >{description}</ SeedField.Description >
) : null }
{renderErrorMessage ? (
< SeedField.ErrorMessage >{errorMessage}</ SeedField.ErrorMessage >
) : null }
</ SeedField.Footer >
) : null }
</ SeedField.Root >
);
},
);
RadioGroup.displayName = "RadioGroup" ;
export interface RadioGroupItemProps extends SeedRadioGroup . ItemProps {
label ?: React . ReactNode ;
}
/**
* @see https://seed-design.io/lynx/components/radio-group
*/
export const RadioGroupItem = React. forwardRef < unknown , RadioGroupItemProps >(
({ label , children , "accessibility-label" : accessibilityLabel , ... otherProps }, ref ) => {
const defaultAccessibilityLabel = typeof label === "string" ? label : undefined ;
return (
< SeedRadioGroup.Item
ref = {ref}
accessibility-label = {accessibilityLabel ?? defaultAccessibilityLabel}
{ ... otherProps}
>
< SeedRadioGroup.ItemControl >
< SeedRadioGroup.ItemIndicator />
</ SeedRadioGroup.ItemControl >
{label != null ? < SeedRadioGroup.ItemLabel >{label}</ SeedRadioGroup.ItemLabel > : null }
{children}
</ SeedRadioGroup.Item >
);
},
);
RadioGroupItem.displayName = "RadioGroupItem" ;
export interface RadiomarkProps extends Omit < SeedRadioGroup . ItemControlProps , "children" > {}
/**
* @see https://seed-design.io/lynx/components/radio-group
*/
export const Radiomark = React. forwardRef < unknown , RadiomarkProps >(( props , ref ) => {
return (
< SeedRadioGroup.ItemControl ref = {ref} { ... props}>
< SeedRadioGroup.ItemIndicator />
</ SeedRadioGroup.ItemControl >
);
});
Radiomark.displayName = "Radiomark" ;
/**
* 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.
*/
RadioGroup은 선택 상태와 함께 Field의 안내 정보를 표시합니다. label, labelWeight, indicator, showRequiredIndicator, description, errorMessage를 사용할 수 있습니다.
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" ;
export function ContactMethod () {
return (
< RadioGroup
label = "선호하는 연락 방법"
indicator = "필수"
description = "연락받을 방법을 선택해 주세요."
defaultValue = "chat"
>
< RadioGroupItem value = "chat" label = "채팅" />
< RadioGroupItem value = "phone" label = "전화" />
</ RadioGroup >
);
}
invalid가 true이고 errorMessage가 있으면 화면에는 description 대신 오류 메시지를 표시합니다.
label?React.ReactNode
indicator?React.ReactNode
showRequiredIndicator?boolean | undefined
description?React.ReactNode
errorMessage?React.ReactNode
value?string | undefined
defaultValue?string | undefined
onValueChange?(( value : string ) => void ) | undefined
style?CSSProperties | undefined
children?React.ReactNode
className?string | undefined
required?boolean | undefined
readOnly?boolean | undefined
label?React.ReactNode
valuestring
disabled?boolean | undefined
style?CSSProperties | undefined
children?React.ReactNode
className?string | undefined
className?string | undefined
style?CSSProperties | undefined
RadioGroup은 기본적으로 접근성 요소이며 accessibility-role-description="radiogroup"을 사용합니다. 그룹이 비활성 상태이면 accessibility-traits="disabled"도 전달합니다. Registry RadioGroup에 문자열 label을 전달하면 같은 내용을 accessibility-label에도 연결합니다.
RadioGroupItem은 기본적으로 accessibility-role-description="radio"를 사용합니다. 선택 상태는 accessibility-value의 selected 또는 not selected로 전달하며, 비활성 상태는 accessibility-traits="disabled"로 전달합니다.
문자열 label을 사용하면 Registry 래퍼가 그 값을 항목의 accessibility-label로 연결합니다. label에 문자열이 아닌 콘텐츠를 넣거나 컴파운드 API를 직접 조합할 때는 각 항목에 accessibility-label을 명시하세요. 필요하면 모든 기본 접근성 prop을 직접 덮어쓸 수 있습니다.
import "./styles" ;
import { VStack, useSeedClassName } from "@seed-design/lynx-react" ;
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" ;
export default function Example () {
const seedClassName = useSeedClassName ({ colorMode: "system" });
return (
< view className = { `${ seedClassName } docs-lynx-radio-group-root` }>
< VStack className = "radio-group-preview" gap = "x5" >
< RadioGroup
accessibility-label = "과일 선택"
defaultValue = "apple"
size = "medium"
tone = "neutral"
>
< RadioGroupItem value = "apple" label = "사과" />
< RadioGroupItem value = "banana" label = "바나나" />
< RadioGroupItem value = "orange" label = "오렌지" />
</ RadioGroup >
< RadioGroup accessibility-label = "색상 선택" defaultValue = "red" size = "large" tone = "neutral" >
< RadioGroupItem value = "red" label = "빨간색" />
< RadioGroupItem value = "blue" label = "파란색" />
< RadioGroupItem value = "green" label = "초록색" />
</ RadioGroup >
</ VStack >
</ view >
);
}
import "./styles" ;
import { VStack, useSeedClassName } from "@seed-design/lynx-react" ;
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" ;
export default function Example () {
const seedClassName = useSeedClassName ({ colorMode: "system" });
return (
< view className = { `${ seedClassName } docs-lynx-radio-group-root` }>
< VStack className = "radio-group-preview" >
< RadioGroup accessibility-label = "과일 선택" defaultValue = "apple" size = "large" tone = "brand" >
< RadioGroupItem value = "apple" label = "사과" />
< RadioGroupItem value = "banana" label = "바나나" />
< RadioGroupItem value = "orange" label = "오렌지" />
</ RadioGroup >
</ VStack >
</ view >
);
}
import "./styles" ;
import { VStack, useSeedClassName } from "@seed-design/lynx-react" ;
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" ;
export default function Example () {
const seedClassName = useSeedClassName ({ colorMode: "system" });
return (
< view className = { `${ seedClassName } docs-lynx-radio-group-root` }>
< VStack className = "radio-group-preview" >
< RadioGroup
accessibility-label = "과일 선택"
defaultValue = "apple"
size = "large"
tone = "neutral"
>
< RadioGroupItem value = "apple" label = "사과" />
< RadioGroupItem value = "banana" label = "바나나" />
< RadioGroupItem value = "orange" label = "오렌지" />
</ RadioGroup >
</ VStack >
</ view >
);
}
React의 항목별 weight와 달리 Lynx의 weight는 그룹 단위 prop입니다. 아래 예제는 굵기마다 그룹을 나눕니다. 두 그룹은 같은 제어 값을 공유하므로 두 항목 중 하나만 선택됩니다.
import "./styles" ;
import { useState } from "@lynx-js/react" ;
import { VStack, useSeedClassName } from "@seed-design/lynx-react" ;
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" ;
export default function Example () {
const seedClassName = useSeedClassName ({ colorMode: "system" });
const [ value , setValue ] = useState ( "regular" );
return (
< view className = { `${ seedClassName } docs-lynx-radio-group-root` }>
< VStack className = "radio-group-preview" >
< RadioGroup
accessibility-label = "글꼴 굵기 선택"
value = {value}
size = "large"
tone = "neutral"
weight = "regular"
onValueChange = {setValue}
>
< RadioGroupItem value = "regular" label = "Regular" />
</ RadioGroup >
< RadioGroup
accessibility-label = "글꼴 굵기 선택"
value = {value}
size = "large"
tone = "neutral"
weight = "bold"
onValueChange = {setValue}
>
< RadioGroupItem value = "bold" label = "Bold" />
</ RadioGroup >
</ VStack >
</ view >
);
}
Lynx의 size도 그룹 단위 prop입니다. 아래 예제는 크기마다 그룹을 나눕니다. 두 그룹은 같은 제어 값을 공유하므로 두 항목 중 하나만 선택됩니다.
import "./styles" ;
import { useState } from "@lynx-js/react" ;
import { VStack, useSeedClassName } from "@seed-design/lynx-react" ;
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" ;
const longLabel =
"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." ;
export default function Example () {
const seedClassName = useSeedClassName ({ colorMode: "system" });
const [ value , setValue ] = useState ( "medium" );
return (
< view className = { `${ seedClassName } docs-lynx-radio-group-root` }>
< VStack className = "radio-group-preview" >
< RadioGroup
accessibility-label = "Long label options"
value = {value}
size = "medium"
tone = "neutral"
onValueChange = {setValue}
>
< RadioGroupItem value = "medium" label = {longLabel} />
</ RadioGroup >
< RadioGroup
accessibility-label = "Long label options"
value = {value}
size = "large"
tone = "neutral"
onValueChange = {setValue}
>
< RadioGroupItem value = "large" label = {longLabel} />
</ RadioGroup >
</ VStack >
</ view >
);
}
RadioGroup의 disabled는 그룹 전체를 비활성화합니다. 개별 RadioGroupItem도 따로 비활성화할 수 있습니다.
import "./styles" ;
import { VStack, useSeedClassName } from "@seed-design/lynx-react" ;
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" ;
export default function Example () {
const seedClassName = useSeedClassName ({ colorMode: "system" });
return (
< view className = { `${ seedClassName } docs-lynx-radio-group-root` }>
< VStack className = "radio-group-preview" >
< RadioGroup
accessibility-label = "Options with disabled"
defaultValue = "option1"
size = "large"
tone = "neutral"
>
< RadioGroupItem value = "option1" label = "Active option" />
< RadioGroupItem value = "option2" label = "Disabled option" disabled />
< RadioGroupItem value = "option3" label = "Another active option" />
</ RadioGroup >
</ VStack >
</ view >
);
}
RadioGroup의 onValueChange로 선택 값 변경을 감지할 수 있습니다. Controlled 모드에서는 value와 onValueChange를 함께 사용합니다.
import "./styles" ;
import { useState } from "@lynx-js/react" ;
import { VStack, useSeedClassName } from "@seed-design/lynx-react" ;
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" ;
export default function Example () {
const seedClassName = useSeedClassName ({ colorMode: "system" });
const [ count , setCount ] = useState ( 0 );
const [ lastValue , setLastValue ] = useState < string | null >( null );
return (
< view className = { `${ seedClassName } docs-lynx-radio-group-root` }>
< VStack className = "radio-group-preview" gap = "x4" >
< RadioGroup
accessibility-label = "Fruit selection"
defaultValue = "apple"
size = "large"
tone = "neutral"
onValueChange = {( value ) => {
setCount (( previous ) => previous + 1 );
setLastValue (value);
}}
>
< RadioGroupItem value = "apple" label = "Apple" />
< RadioGroupItem value = "banana" label = "Banana" />
< RadioGroupItem value = "orange" label = "Orange" />
</ RadioGroup >
< text className = "radio-group-preview__status" >
onValueChange called: {count} times, last value: {lastValue ?? "-" }
</ text >
</ VStack >
</ view >
);
}
React Hook Form은 HTML 폼과 input을 전제로 하므로 Lynx에서는 같은 예제를 실행할 수 없습니다. Lynx에서는 value와 onValueChange로 선택 값을 제어하세요. 제출과 검증은 앱의 상태 관리와 요청 흐름에서 처리합니다.
Registry의 Radiomark와 @seed-design/lynx-react의 컴파운드 API를 함께 사용하면 선택 상태를 공유하면서 항목의 배치를 바꿀 수 있습니다. Radiomark는 RadioGroup.Item 안에서만 사용할 수 있습니다.
import "./styles" ;
import {
HStack,
RadioGroup as RadioGroupPrimitive,
VStack,
useSeedClassName,
} from "@seed-design/lynx-react" ;
import { RadioGroup, Radiomark } from "@/components/ui/radio-group" ;
export default function Example () {
const seedClassName = useSeedClassName ({ colorMode: "system" });
return (
< view className = { `${ seedClassName } docs-lynx-radio-group-root` }>
< VStack className = "radio-group-preview" >
< RadioGroup
accessibility-label = "Weight selection"
defaultValue = "medium"
size = "large"
tone = "neutral"
>
< HStack gap = "x6" >
< RadioGroupPrimitive.Item accessibility-label = "regular" value = "regular" >
< VStack gap = "x2" align = "center" >
< Radiomark />
< RadioGroupPrimitive.ItemLabel >regular</ RadioGroupPrimitive.ItemLabel >
</ VStack >
</ RadioGroupPrimitive.Item >
< RadioGroupPrimitive.Item accessibility-label = "medium" value = "medium" >
< VStack gap = "x2" align = "center" >
< Radiomark />
< RadioGroupPrimitive.ItemLabel >medium</ RadioGroupPrimitive.ItemLabel >
</ VStack >
</ RadioGroupPrimitive.Item >
< RadioGroupPrimitive.Item accessibility-label = "bold" value = "bold" >
< VStack gap = "x2" align = "center" >
< Radiomark />
< RadioGroupPrimitive.ItemLabel >bold</ RadioGroupPrimitive.ItemLabel >
</ VStack >
</ RadioGroupPrimitive.Item >
</ HStack >
</ RadioGroup >
</ VStack >
</ view >
);
}
label, description, errorMessage 등의 RadioGroupField 관련 prop을 사용할 수 있습니다.
import "./styles" ;
import { useState } from "@lynx-js/react" ;
import { ActionButton, HStack, VStack, useSeedClassName } from "@seed-design/lynx-react" ;
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" ;
export default function Example () {
const seedClassName = useSeedClassName ({ colorMode: "system" });
const [ contact , setContact ] = useState ( "email" );
const [ firstErrorMessage , setFirstErrorMessage ] = useState < string | undefined >();
const [ option , setOption ] = useState ( "option1" );
const [ secondErrorMessage , setSecondErrorMessage ] = useState < string | undefined >();
const handleFirstSubmit = () => {
setFirstErrorMessage (contact === "email" ? "이메일은 선택할 수 없습니다." : undefined );
};
const handleSecondSubmit = () => {
setSecondErrorMessage (option === "option1" ? "옵션 1은 선택할 수 없습니다." : undefined );
};
return (
< view className = { `${ seedClassName } docs-lynx-radio-group-root` }>
< VStack className = "radio-group-preview" >
< HStack width = "full" gap = "x8" align = "flex-start" >
< VStack style = {{ flex: 1 }} gap = "spacingY.componentDefault" >
< RadioGroup
label = "선호하는 연락 방법"
indicator = "필수"
description = "이메일을 선택하고 제출해보세요."
value = {contact}
onValueChange = {setContact}
invalid = {firstErrorMessage != null }
errorMessage = {firstErrorMessage}
tone = "neutral"
size = "large"
>
< RadioGroupItem value = "email" label = "이메일" />
< RadioGroupItem value = "phone" label = "전화" />
< RadioGroupItem value = "sms" label = "문자" />
</ RadioGroup >
< ActionButton variant = "neutralSolid" bindtap = {handleFirstSubmit}>
제출
</ ActionButton >
</ VStack >
< VStack style = {{ flex: 1 }} gap = "spacingY.componentDefault" >
< RadioGroup
label = "필수 선택"
labelWeight = "bold"
showRequiredIndicator
description = "옵션 1을 선택하고 제출해보세요."
value = {option}
onValueChange = {setOption}
invalid = {secondErrorMessage != null }
errorMessage = {secondErrorMessage}
tone = "neutral"
size = "large"
>
< RadioGroupItem value = "option1" label = "옵션 1" />
< RadioGroupItem value = "option2" label = "옵션 2" disabled />
< RadioGroupItem value = "option3" label = "옵션 3" />
</ RadioGroup >
< ActionButton variant = "neutralSolid" bindtap = {handleSecondSubmit}>
제출
</ ActionButton >
</ VStack >
</ HStack >
</ VStack >
</ view >
);
}
Lynx RadioGroupItem은 누르는 동안 Radiomark의 배경색을 바꿉니다. React처럼 크기를 줄이지 않으므로 --seed-radiomark-feedback-scale CSS 변수도 사용하지 않습니다.
Lynx RadioGroup은 React RadioGroup과 다음과 같은 차이가 있습니다.
오류 안내 렌더링 : React는 오류가 표시될 때 description을 접근성 트리에 남기지만, Lynx에는 DOM의 visually hidden 처리 방식이 없어 화면과 접근성 트리에서 description을 제외하고 errorMessage만 렌더링합니다.
아이콘 렌더링 방식 : 기본 RadioGroup.ItemIndicator는 <view> 점으로 렌더됩니다. 사용자 지정 아이콘을 전달하려면 @karrotmarket/lynx-monochrome-icon을 별도로 설치하고 단색 아이콘 컴포넌트를 사용하세요. <svg>를 직접 주입하는 방식은 Lynx에서 지원하지 않습니다.
이벤트 핸들링 : onChange 대신 onValueChange(value: string)만 노출합니다. tap 핸들러는 item 내부에서 소유합니다.
Pressed 상태 : 웹의 크기 축소 대신 내부 press state가 Radiomark의 배경색을 바꿉니다.
렌더링 요소 : HTML <label> / <input type="radio"> 대신 네이티브 <view> / <text> 요소를 렌더링합니다.
Strict containment : RadioGroupItem, Radiomark, RadioGroup.ItemControl, RadioGroup.ItemIndicator, RadioGroup.ItemLabel은 반드시 RadioGroup.Root와 RadioGroup.Item의 적절한 context 안에서 사용해야 합니다. Radiomark는 standalone primitive가 아니라 item context 안에서 ItemControl과 ItemIndicator를 묶는 snippet helper입니다.
사용자 지정 아이콘 패키지는 Registry 기본 설치에 포함되지 않습니다.
bun add @karrotmarket/lynx-monochrome-icon@^1.20.0
현재 Lynx 플랫폼 제약으로 다음 기능이 지원되지 않습니다.
기능 웹 대응 설명 React Hook Form HTML form 라이브러리 value와 onValueChange 및 앱 수준 제출 로직으로 대체RadioGroupItemHiddenInput<input type="radio">Lynx에 HTML form 제출 모델 없음 namenative form field name Lynx에 native form 제출 모델 없음 focus / focusVisible키보드 포커스 Lynx에 키보드 포커스 개념 없음 onChange (raw DOM event)React.ChangeEvent의미 없음. onValueChange로 대체