Select (Single)
드롭다운 목록에서 하나의 값을 선택하는 컴포넌트.
개요
Select는 드롭다운 목록에서 값을 선택하는 compound component입니다.
- Compound composition —
Root,Trigger,Value,Icon,Indicator,Positioner,Popup,Item,ItemText,Group,GroupLabel,Separator,Action,Overflow등 - Base UI 기반 — Floating UI 포지셔닝, collision detection, 키보드 내비게이션 자동 처리
- 3가지
size—sm(28px),md(32px, 기본),lg(40px) - 반응형
size—{ mobile: 'sm', tablet: 'md' }객체 지원 - Field 연동 —
Field.Root안에 배치 시status,disabled,readOnly자동 전파 - Indicator 자동 렌더 — Trigger에 ChevronDown이 자동 포함,
<Select.Indicator>로 override 가능 $cssprop — 모든 서브컴포넌트에서 rainbow-sprinkles 토큰 기반 스타일링renderprop — base-ui 기반 다형성 렌더링
Trigger 내부 구조
<Select.Trigger>
[Select.Icon] ← optional leading icon
<Select.Value /> ← flex: 1, overflow: hidden
[Select.Overflow] ← optional +N 표시 (Value 바깥)
Select.Indicator ← 자동 렌더 (소비자가 배치하지 않아도 됨, override 가능)
</Select.Trigger>children 순서가 시각적 순서를 결정합니다. Select.Indicator는 소비자가 직접 배치하지 않아도 Trigger가 자동으로 끝에 추가합니다.
접근성
- WCAG 4.1.2 —
role="listbox"+aria-expanded자동 연결 - 키보드 —
Enter/Space로 열기,ArrowUp/ArrowDown으로 탐색,Escape로 닫기 - 타이핑 검색 — 문자 입력 시 해당 항목으로 이동
- 포커스 관리 — popup 열릴 때 선택된 항목으로 자동 포커스
언제 사용하나요
- 5개 이상의 옵션에서 하나 또는 여러 개를 선택
- 폼에서 카테고리, 상태, 필터 등을 선택
- 공간이 제한적일 때 (Radio/Checkbox 목록 대신)
언제 사용하면 안 되나요
- 2~4개의 옵션 →
Radio또는SegmentedControl - 자유 텍스트 입력 + 검색 → Combobox (향후 제공)
- 네비게이션 메뉴 → Dropdown Menu
- on/off 토글 →
Switch
Usage
기본 사용법
<Select.Root defaultValue="option-2"> <Select.Trigger $css={{ width: '240px' }}> <Select.Value placeholder="옵션 선택" /> </Select.Trigger> <Select.Portal> <Select.Positioner> <Select.Popup> <Select.Item value="option-1"><Select.ItemText>Option 1</Select.ItemText></Select.Item> <Select.Item value="option-2"><Select.ItemText>Option 2</Select.ItemText></Select.Item> <Select.Item value="option-3"><Select.ItemText>Option 3</Select.ItemText></Select.Item> </Select.Popup> </Select.Positioner> </Select.Portal> </Select.Root>
크기
<VStack $css={{ gap: '$spacing-400' }}> {['sm', 'md', 'lg'].map((size) => ( <Select.Root key={size} size={size} defaultValue="option-1"> <Select.Trigger $css={{ width: '240px' }}> <Select.Value /> </Select.Trigger> <Select.Portal> <Select.Positioner> <Select.Popup> <Select.Item value="option-1"><Select.ItemText>{size} 크기</Select.ItemText></Select.Item> <Select.Item value="option-2"><Select.ItemText>Option 2</Select.ItemText></Select.Item> </Select.Popup> </Select.Positioner> </Select.Portal> </Select.Root> ))} </VStack>
그룹과 구분선
Select.Group, Select.GroupLabel, Select.Separator로 옵션을 그룹화합니다.
<Select.Root> <Select.Trigger $css={{ width: '240px' }}> <Select.Value placeholder="과일/채소 선택" /> </Select.Trigger> <Select.Portal> <Select.Positioner> <Select.Popup> <Select.Group> <Select.GroupLabel>과일</Select.GroupLabel> <Select.Item value="apple"><Select.ItemText>사과</Select.ItemText></Select.Item> <Select.Item value="banana"><Select.ItemText>바나나</Select.ItemText></Select.Item> </Select.Group> <Select.Separator /> <Select.Group> <Select.GroupLabel>채소</Select.GroupLabel> <Select.Item value="carrot"><Select.ItemText>당근</Select.ItemText></Select.Item> <Select.Item value="spinach"><Select.ItemText>시금치</Select.ItemText></Select.Item> </Select.Group> </Select.Popup> </Select.Positioner> </Select.Portal> </Select.Root>
Leading Icon
Select.Icon을 Value 앞에 배치하면 leading icon이 됩니다. size에 따라 아이콘 크기가 자동 조절됩니다.
<Select.Root defaultValue="option-1"> <Select.Trigger $css={{ width: '280px' }}> <Select.Icon><IconAccountFilled /></Select.Icon> <Select.Value placeholder="담당자 선택" /> </Select.Trigger> <Select.Portal> <Select.Positioner> <Select.Popup> <Select.Item value="option-1"><Select.ItemText>홍길동</Select.ItemText></Select.Item> <Select.Item value="option-2"><Select.ItemText>김철수</Select.ItemText></Select.Item> </Select.Popup> </Select.Positioner> </Select.Portal> </Select.Root>
Indicator 커스터마이징
기본 ChevronDown은 popup open 시 자동 회전합니다. <Select.Indicator>로 커스텀 아이콘을 넣으면 회전하지 않습니다. 커스텀 아이콘도 회전시키려면 data-rotate 속성을 추가합니다.
<HStack $css={{ gap: '$spacing-400' }}> <Select.Root defaultValue="opt-1"> <Select.Trigger $css={{ width: '200px' }}> <Select.Value /> </Select.Trigger> <Select.Portal> <Select.Positioner> <Select.Popup> <Select.Item value="opt-1"><Select.ItemText>기본 Chevron</Select.ItemText></Select.Item> <Select.Item value="opt-2"><Select.ItemText>Option 2</Select.ItemText></Select.Item> </Select.Popup> </Select.Positioner> </Select.Portal> </Select.Root> <Select.Root defaultValue="opt-1"> <Select.Trigger $css={{ width: '200px' }}> <Select.Value /> <Select.Indicator><IconTuneFilled size="16px" /></Select.Indicator> </Select.Trigger> <Select.Portal> <Select.Positioner> <Select.Popup> <Select.Item value="opt-1"><Select.ItemText>커스텀 Icon</Select.ItemText></Select.Item> <Select.Item value="opt-2"><Select.ItemText>Option 2</Select.ItemText></Select.Item> </Select.Popup> </Select.Positioner> </Select.Portal> </Select.Root> </HStack>
상태 (disabled, readOnly, status)
<VStack $css={{ gap: '$spacing-400' }}> <Select.Root disabled defaultValue="opt-1"> <Select.Trigger $css={{ width: '240px' }}> <Select.Value /> </Select.Trigger> <Select.Portal> <Select.Positioner> <Select.Popup> <Select.Item value="opt-1"><Select.ItemText>Disabled</Select.ItemText></Select.Item> </Select.Popup> </Select.Positioner> </Select.Portal> </Select.Root> <Select.Root readOnly defaultValue="opt-1"> <Select.Trigger $css={{ width: '240px' }}> <Select.Value /> </Select.Trigger> <Select.Portal> <Select.Positioner> <Select.Popup> <Select.Item value="opt-1"><Select.ItemText>ReadOnly</Select.ItemText></Select.Item> </Select.Popup> </Select.Positioner> </Select.Portal> </Select.Root> {['error', 'warning', 'success'].map((status) => ( <Select.Root key={status} status={status} defaultValue="opt-1"> <Select.Trigger $css={{ width: '240px' }}> <Select.Value /> </Select.Trigger> <Select.Portal> <Select.Positioner> <Select.Popup> <Select.Item value="opt-1"><Select.ItemText>{status}</Select.ItemText></Select.Item> </Select.Popup> </Select.Positioner> </Select.Portal> </Select.Root> ))} </VStack>
Field와 함께 사용
Field.Root 안에 배치하면 status, disabled, readOnly가 자동 전파됩니다.
() => { const [value, setValue] = React.useState(null); return ( <Field.Root status={!value ? 'error' : 'none'}> <Field.Label required>카테고리</Field.Label> <Select.Root value={value ?? undefined} onValueChange={(v) => setValue(v)}> <Select.Trigger $css={{ width: '280px' }}> <Select.Value placeholder="카테고리 선택" /> </Select.Trigger> <Select.Portal> <Select.Positioner> <Select.Popup> <Select.Item value="design"><Select.ItemText>디자인</Select.ItemText></Select.Item> <Select.Item value="develop"><Select.ItemText>개발</Select.ItemText></Select.Item> <Select.Item value="marketing"><Select.ItemText>마케팅</Select.ItemText></Select.Item> </Select.Popup> </Select.Positioner> </Select.Portal> </Select.Root> {!value ? <Field.Message>카테고리를 선택해 주세요.</Field.Message> : <Field.Description>선택된 카테고리: {value}</Field.Description> } </Field.Root> ); }
긴 텍스트 (Ellipsis)
Select.ItemText에 $css로 ellipsis를 적용합니다. Popup 너비는 기본적으로 Trigger 너비에 맞춰집니다.
<Select.Root> <Select.Trigger $css={{ width: '240px' }}> <Select.Value placeholder="프로젝트 선택" /> </Select.Trigger> <Select.Portal> <Select.Positioner> <Select.Popup> <Select.Item value="1"> <Select.ItemText $css={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}> 매우 긴 프로젝트 이름이 들어가는 첫 번째 항목입니다 </Select.ItemText> </Select.Item> <Select.Item value="2"> <Select.ItemText $css={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}> 두 번째 항목도 역시 매우 긴 텍스트를 가지고 있습니다 </Select.ItemText> </Select.Item> <Select.Item value="3"> <Select.ItemText>짧은 항목</Select.ItemText> </Select.Item> </Select.Popup> </Select.Positioner> </Select.Portal> </Select.Root>
$css 커스텀
<Select.Root defaultValue="opt-1"> <Select.Trigger $css={{ width: '280px', borderRadius: '$radius-300' }}> <Select.Value /> </Select.Trigger> <Select.Portal> <Select.Positioner> <Select.Popup $css={{ borderRadius: '$radius-300', padding: '$spacing-300' }}> <Select.Item value="opt-1" $css={{ borderRadius: '$radius-200' }}> <Select.ItemText>커스텀 스타일 1</Select.ItemText> </Select.Item> <Select.Item value="opt-2" $css={{ borderRadius: '$radius-200' }}> <Select.ItemText>커스텀 스타일 2</Select.ItemText> </Select.Item> </Select.Popup> </Select.Positioner> </Select.Portal> </Select.Root>
CoreSelectPrim에서 마이그레이션
Before
<CoreSelectPrim.Root label="카테고리" required tooltip>
<CoreSelectPrim.Trigger placeholder="선택하세요">
{selectedLabel}
</CoreSelectPrim.Trigger>
<CoreSelectPrim.Content>
<CoreSelectPrim.Item value="design">디자인</CoreSelectPrim.Item>
<CoreSelectPrim.Item value="develop">개발</CoreSelectPrim.Item>
</CoreSelectPrim.Content>
</CoreSelectPrim.Root>After
<Field.Root>
<Field.Label required>카테고리</Field.Label>
<Select.Root value={value} onValueChange={setValue}>
<Select.Trigger>
<Select.Value placeholder="선택하세요" />
</Select.Trigger>
<Select.Portal>
<Select.Positioner>
<Select.Popup>
<Select.Item value="design"><Select.ItemText>디자인</Select.ItemText></Select.Item>
<Select.Item value="develop"><Select.ItemText>개발</Select.ItemText></Select.Item>
</Select.Popup>
</Select.Positioner>
</Select.Portal>
</Select.Root>
</Field.Root>변경 사항
| Before (CoreSelectPrim) | After (Select) |
|---|---|
label, required, tooltip (Root에 내장) | Field.Root + Field.Label 분리 |
<Trigger placeholder> | <Select.Value placeholder> |
<Content> | <Select.Portal> + <Select.Positioner> + <Select.Popup> |
<Item value>텍스트</Item> | <Select.Item value><Select.ItemText>텍스트</Select.ItemText></Select.Item> |
| className/style (정적) | className/style (state callback) |
| - | $css prop 지원 |
| - | render prop (다형성) |
| - | multiple 다중 선택 |
| - | Select.Overflow 자동 카운트 |
| - | Select.Indicator 자동 렌더 + override |
| - | Select.Icon leading icon |
Props
공통 Props —
$css,render,className,style는 모든 서브컴포넌트에서 지원됩니다. useRenderComponent 가이드 →
Select.Root
상태 관리 wrapper. DOM을 렌더링하지 않습니다.
Prop
Type
Select.Trigger
드롭다운을 여는 버튼. Select.Indicator(ChevronDown)가 자동으로 children 끝에 렌더됩니다. <Select.Indicator>를 직접 배치하면 자동 렌더가 생략됩니다.
Select.Value
선택된 값을 표시하는 영역. overflow: hidden이 기본 적용됩니다.
Prop
Type
Select.Icon
범용 아이콘 슬롯. 주로 Trigger 내 leading icon으로 사용합니다. size에 따라 아이콘 크기가 자동 조절됩니다 (sm/md: 16px, lg: 20px).
Prop
Type
Select.Indicator
Trailing chevron. Trigger가 자동으로 렌더합니다. children으로 커스텀 아이콘을 지정하면 기본 ChevronDown이 대체됩니다. 기본 아이콘은 popup open 시 data-rotate로 180도 회전합니다.
Prop
Type
Select.Positioner
Floating UI 포지셔닝 wrapper.
Prop
Type
Select.Popup
드롭다운 팝업 컨테이너. 기본 너비가 Trigger 너비(--anchor-width)에 맞춰집니다.
Select.Item
개별 선택 항목.
Prop
Type
Select.ItemText
항목 텍스트. flexGrow: 1, minWidth: 0이 기본 적용되어 ellipsis를 $css로 opt-in 가능합니다.
Select.Overflow
다중 선택 시 Value에서 숨겨진 항목 수를 자동 감지합니다. Value 바깥, Trigger 안에 배치합니다. Context를 통해 Value의 DOM을 측정합니다.
Prop
Type
Select.Action
값에 참여하지 않는 시각적 항목 (예: "전체 선택" 버튼). role="presentation"으로 렌더됩니다.
Select.ScrollUpArrow / Select.ScrollDownArrow
Popup 내 스크롤 화살표. 긴 목록에서 스크롤 가능 여부를 시각적으로 표시합니다.
스타일
Size Variants
| Size | min-height | Typography | Icon 크기 |
|---|---|---|---|
sm | 28px | body-1 | 16px |
md | 32px | body-2 | 16px |
lg | 40px | body-2 | 20px |
Trigger States
| State | Border | Background |
|---|---|---|
| default | border-1 | background-1 |
| hover | border-2 | background-2 |
| focus / open | primary-50 | background-1 |
| readOnly | border-2 | background-4 |
| disabled | border-2 | background-4 |
| error | support-error-3 | background-1 |
| warning | support-warning-3 | background-1 |
| success | support-success-3 | background-1 |
Item States
| State | Background | Color |
|---|---|---|
| default | transparent | text-1 |
| highlighted | primary-10 | primary-100 |
| disabled | toggle-disabled-bg | toggle-disabled-text |
State Types
Prop
Type