Select (Single)
드롭다운 목록에서 하나의 값을 선택하는 compound component.
개요
Select는 Base UI Select를 로직 레이어로, Dropdown 컴포넌트를 비주얼 레이어로 위임하는 compound component입니다. Trigger가 열리면 Floating UI가 포지셔닝을 담당하고, 키보드 내비게이션과 포커스 관리는 Base UI가 처리합니다.
- Dropdown 위임 —
Trigger,Item,Icon,Indicator,Separator,GroupLabel등 비주얼 렌더링은 모두 Dropdown 컴포넌트에 위임 - Compound composition —
Root,Trigger,Value,Icon,Indicator,Portal,Backdrop,Positioner,Popup,List,Item,ItemCheckbox,ItemRadio,ItemText,Group,GroupLabel,Separator,Action,Button,Overflow,Arrow - 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 — 모든 서브컴포넌트에서 디자인 토큰 기반 스타일링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가 자동으로 끝에 추가합니다.
언제 사용하나요
- 5개 이상의 옵션에서 하나를 선택할 때
- 폼에서 카테고리, 상태, 필터 같은 단일 값을 입력받을 때
- 공간이 제한적이어서 Radio 목록을 펼칠 수 없을 때
언제 사용하면 안 되나요
- 2~4개의 옵션 →
Radio또는SegmentedControl사용 - 자유 텍스트 입력 + 검색 → Combobox (향후 제공)
- 네비게이션 메뉴 → Dropdown Menu
- on/off 토글 →
Switch - 여러 개 동시 선택 → Select (Multiple)
접근성
키보드 동작
| 키 | 동작 |
|---|---|
Enter / Space | Trigger 포커스 시 popup 열기 |
ArrowUp / ArrowDown | 항목 간 이동 |
Home / End | 첫 번째 / 마지막 항목으로 이동 |
Enter | 강조된 항목 선택 후 popup 닫기 |
Escape | popup 닫기, Trigger로 포커스 복귀 |
| 문자 입력 | 해당 문자로 시작하는 항목으로 이동 |
스크린리더 동작
Base UI가 role="listbox", aria-expanded, aria-selected, aria-activedescendant를 자동으로 관리합니다. Select.Item의 label prop으로 키보드 검색 텍스트를 별도 지정할 수 있습니다.
WCAG
| 기준 | 적용 |
|---|---|
| 1.3.1 Info and Relationships | role="listbox" + aria-selected |
| 2.1.1 Keyboard | 전체 키보드 동작 지원 |
| 2.4.7 Focus Visible | focus-visible 시 primary-50 아웃라인 |
| 4.1.2 Name, Role, Value | aria-expanded, aria-disabled, aria-readonly 자동 관리 |
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>
크기
size prop으로 sm, md, lg 세 가지 크기를 사용합니다.
<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>
크기 Override (own > ctx)
Root.size는 Trigger / Item / Action / Button에 컨텍스트로 전파됩니다. 각 sub-component에 자체 size를 명시하면 컨텍스트보다 우선합니다.
<Select.Root size="md"> <Select.Trigger $css={{ width: '240px' }}> <Select.Value placeholder="옵션 선택 (md ctx)" /> </Select.Trigger> <Select.Portal> <Select.Positioner> <Select.Popup> <Select.Item value="a"> <Select.ItemText>A (md ctx)</Select.ItemText> </Select.Item> <Select.Item value="b" size="sm"> <Select.ItemText>B (sm own)</Select.ItemText> </Select.Item> <Select.Item value="c" size="lg"> <Select.ItemText>C (lg own)</Select.ItemText> </Select.Item> <Select.Action size="sm">전체 선택 (sm own)</Select.Action> <Select.Button size="lg">적용 (lg own)</Select.Button> </Select.Popup> </Select.Positioner> </Select.Portal> </Select.Root>
그룹과 구분선
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에 따라 아이콘 크기가 자동 조절됩니다 (sm/md: 16px, lg: 20px).
<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>
Avatar와 함께
Select.Item 안에 Avatar.Root를 children으로 배치하면 사용자 선택 UI를 만들 수 있습니다. 두 줄 정보(이름 + 역할)는 VStack으로 묶고, Select.Value에서도 selected user의 Avatar를 함께 렌더링할 수 있습니다.
() => { const users = [ { value: 'user-1', name: '김철수', role: '디자이너' }, { value: 'user-2', name: '박영희', role: '엔지니어' }, { value: 'user-3', name: '이민수', role: 'PM' }, ]; const [value, setValue] = React.useState(null); const selected = users.find((u) => u.value === value); return ( <Select.Root value={value} onValueChange={(v) => setValue(v)}> <Select.Trigger $css={{ width: '300px' }}> <Select.Value> {selected ? ( <HStack $css={{ gap: '$spacing-200', alignItems: 'center' }}> <Avatar.Root size="xs" name={selected.name}> <Avatar.Fallback /> </Avatar.Root> <Typo variant="$body-2" render={<span />}>{selected.name}</Typo> </HStack> ) : '담당자 선택'} </Select.Value> </Select.Trigger> <Select.Portal> <Select.Positioner> <Select.Popup> {users.map((user) => ( <Select.Item key={user.value} value={user.value} $css={{ padding: '8px' }}> <HStack $css={{ gap: '$spacing-250', alignItems: 'center', width: '100%' }}> <Avatar.Root size="md" name={user.name}> <Avatar.Fallback /> </Avatar.Root> <VStack $css={{ gap: '$spacing-25' }}> <Typo variant="$heading-1" render={<span />}>{user.name}</Typo> <Typo variant="$caption-1" $css={{ color: '$text-3' }} render={<span />}>{user.role}</Typo> </VStack> </HStack> </Select.Item> ))} </Select.Popup> </Select.Positioner> </Select.Portal> </Select.Root> ); }
Indicator 커스터마이징
기본 ChevronDown은 popup open 시 자동으로 180도 회전합니다. <Select.Indicator>로 커스텀 아이콘을 넣으면 회전하지 않습니다.
<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="$icon-md" /></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>
비활성 아이템에 사유 안내 (Tooltip)
Select.Item disabled는 hover/focus 이벤트가 살아 있어 Tooltip.Trigger render={<Select.Item disabled />} 패턴으로 비활성 사유를 안내할 수 있습니다.
<Select.Root> <Select.Trigger $css={{ width: '240px' }}> <Select.Value placeholder="옵션 선택" /> </Select.Trigger> <Select.Portal> <Select.Positioner> <Select.Popup> <Select.Item value="1"> <Select.ItemText>Option 1 (활성)</Select.ItemText> </Select.Item> <Tooltip.Root> <Tooltip.Trigger render={<Select.Item value="2" disabled />}> <Select.ItemText>Option 2 (비활성)</Select.ItemText> </Tooltip.Trigger> <Tooltip.Content side="right"> <Tooltip.Arrow /> 이미 발송된 옵션입니다 </Tooltip.Content> </Tooltip.Root> <Select.Item value="3"> <Select.ItemText>Option 3 (활성)</Select.ItemText> </Select.Item> </Select.Popup> </Select.Positioner> </Select.Portal> </Select.Root>
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> ); }
Radio 스타일 선택
Select.Item 안에 Label.Root + Radio + Label.Text를 배치하면 radio indicator가 포함된 선택 UI를 만듭니다.
() => { const [value, setValue] = React.useState('option-1'); const options = [ { value: 'option-1', label: 'Option 1' }, { value: 'option-2', label: 'Option 2' }, { value: 'option-3', label: 'Option 3' }, ]; return ( <Select.Root value={value} onValueChange={(v) => setValue(v)}> <Select.Trigger $css={{ width: '280px' }}> <Select.Value>{options.find((o) => o.value === value)?.label ?? '옵션 선택'}</Select.Value> </Select.Trigger> <Select.Portal> <Select.Positioner> <Select.Popup> {options.map((opt) => ( <Select.Item key={opt.value} value={opt.value}> <Label.Root> <Radio value={opt.value} checked={opt.value === value} onChange={() => undefined} size="sm" /> <Label.Text>{opt.label}</Label.Text> </Label.Root> </Select.Item> ))} </Select.Popup> </Select.Positioner> </Select.Portal> </Select.Root> ); }
검색 (Search/Filter)
TextInput을 Popup 상단에 배치하고 onOpenChange에서 닫힐 때 키워드를 초기화합니다. Positioner에 disableAnchorTracking을 추가해야 Trigger 너비에 종속되지 않습니다.
() => { const [value, setValue] = React.useState(null); const [keyword, setKeyword] = React.useState(''); const cities = [ { value: 'seoul', label: '서울' }, { value: 'busan', label: '부산' }, { value: 'daegu', label: '대구' }, { value: 'incheon', label: '인천' }, { value: 'gwangju', label: '광주' }, { value: 'daejeon', label: '대전' }, { value: 'jeju', label: '제주' }, ]; const filtered = cities.filter((opt) => opt.label.includes(keyword)); return ( <Select.Root value={value} onValueChange={(v) => setValue(v)} onOpenChange={(open) => { if (!open) setKeyword(''); }} > <Select.Trigger $css={{ width: '240px' }}> <Select.Value placeholder="도시 선택" /> </Select.Trigger> <Select.Portal> <Select.Positioner disableAnchorTracking> <Select.Popup $css={{ height: '300px' }} style={{ display: 'flex', flexDirection: 'column' }}> <Box $css={{ paddingBottom: '$spacing-200' }} style={{ flexShrink: 0, borderBottom: '1px solid #eee' }}> <TextInput.Root size="md"> <TextInput.Icon><IconSearchOutline /></TextInput.Icon> <TextInput.Input placeholder="도시 검색" value={keyword} onChange={(e) => setKeyword(e.currentTarget.value)} /> </TextInput.Root> </Box> <Box style={{ flex: 1, overflowY: 'auto' }}> {filtered.map((opt) => ( <Select.Item key={opt.value} value={opt.value}> <Label.Root> <Radio value={opt.value} checked={opt.value === value} onChange={() => undefined} size="sm" /> <Label.Text>{opt.label}</Label.Text> </Label.Root> </Select.Item> ))} {filtered.length === 0 && ( <Box $css={{ padding: '$spacing-400', textAlign: 'center' }}> <Typo variant="$body-1" $css={{ color: '$text-5' }}>검색 결과가 없습니다.</Typo> </Box> )} </Box> </Select.Popup> </Select.Positioner> </Select.Portal> </Select.Root> ); }
커스텀 Value (Tag)
Select.Value의 children에 Tag를 렌더링하면 선택 값을 태그 형태로 표시할 수 있습니다.
() => { const [selected, setSelected] = React.useState('design'); const options = { engineering: { label: 'Engineering', color: 'blue' }, design: { label: 'Design', color: 'orange' }, marketing: { label: 'Marketing', color: 'primary' }, }; return ( <Select.Root value={selected} onValueChange={(v) => setSelected(v)}> <Select.Trigger $css={{ width: '240px' }}> <Select.Value> {selected && options[selected] ? ( <Tag.Root color={options[selected].color} size="sm"> <Tag.Text>{options[selected].label}</Tag.Text> </Tag.Root> ) : '부서 선택'} </Select.Value> </Select.Trigger> <Select.Portal> <Select.Positioner> <Select.Popup> {Object.entries(options).map(([v, { label }]) => ( <Select.Item key={v} value={v}> <Select.ItemText>{label}</Select.ItemText> </Select.Item> ))} </Select.Popup> </Select.Positioner> </Select.Portal> </Select.Root> ); }
Trigger에 사유 안내 (Tooltip)
전체 Select가 비활성일 때 사유를 안내하려면 Tooltip.Trigger render={<Select.Trigger disabled focusableWhenDisabled />} 패턴을 사용합니다. focusableWhenDisabled는 키보드 / 터치 접근성도 확보합니다.
<Select.Root> <Tooltip.Root> <Tooltip.Trigger render={<Select.Trigger disabled focusableWhenDisabled $css={{ minWidth: '280px' }} />}> <Select.Value placeholder="선택 불가" /> </Tooltip.Trigger> <Tooltip.Content> <Tooltip.Arrow /> 현재 변경할 수 없습니다 </Tooltip.Content> </Tooltip.Root> <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.Popup> </Select.Positioner> </Select.Portal> </Select.Root>
Action (선택 초기화 등 보조 동작)
Select.Action은 value 없이 onClick으로만 동작하는 popup-내 보조 항목입니다. danger 플래그로 위험 동작(삭제/초기화)을 표시합니다.
() => { const [value, setValue] = React.useState('option-1'); return ( <Select.Root value={value} onValueChange={(v) => setValue(v)}> <Select.Trigger $css={{ width: '280px' }}> <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.Separator /> <Select.Action danger onClick={() => setValue('')}> <IconTrashOutline size="$icon-md" /> 선택 초기화 </Select.Action> </Select.Popup> </Select.Positioner> </Select.Portal> </Select.Root> ); }
Positioning (side / align)
Select.Positioner의 side / align props로 popup 위치를 제어합니다. 기본은 side="bottom" align="center"이며, 뷰포트 충돌 시 자동으로 반전됩니다.
<HStack $css={{ gap: '$spacing-800', padding: '$spacing-800' }}> {['bottom', 'top'].map((side) => ( <VStack key={side} $css={{ gap: '$spacing-200', alignItems: 'center' }}> <Typo variant="$caption-2" $css={{ color: '$text-3' }}>side="{side}"</Typo> <Select.Root> <Select.Trigger $css={{ width: '180px' }}> <Select.Value placeholder="선택" /> </Select.Trigger> <Select.Portal> <Select.Positioner side={side} align="center"> <Select.Popup> <Select.Item value="a"><Select.ItemText>옵션 A</Select.ItemText></Select.Item> <Select.Item value="b"><Select.ItemText>옵션 B</Select.ItemText></Select.Item> <Select.Item value="c"><Select.ItemText>옵션 C</Select.ItemText></Select.Item> </Select.Popup> </Select.Positioner> </Select.Portal> </Select.Root> </VStack> ))} </HStack>
반응형 크기
size에 반응형 객체를 전달하면 뷰포트에 따라 크기가 변경됩니다.
<VStack $css={{ gap: '$spacing-400' }}> <Typo variant="$caption-2" $css={{ color: '$text-3' }}> 브라우저 너비를 조절해 보세요 (mobile: sm → desktop: lg) </Typo> <Select.Root defaultValue="option-1" size={{ mobile: 'sm', desktop: 'lg' }}> <Select.Trigger $css={{ width: '280px' }}> <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>
Viewport Constraint (popup이 화면을 넘지 않게)
Select.Popup은 내부적으로 Dropdown.Popup을 래핑합니다. Base UI Positioner가 노출하는 --available-height / --available-width CSS 변수를 max-height / max-width로 클램프하므로, 좁은 viewport에서도 popup이 잘리지 않고 내부 스크롤로 처리됩니다.
Select.Popup은 별도로 --anchor-width(트리거 폭)를 받아 너비를 트리거에 맞춥니다. max-width: var(--available-width)는 이 anchor-width가 viewport를 넘을 때 안전망 역할을 합니다. 소비자가 따로 설정할 건 없습니다.
Ellipsis Overflow (소비자 opt-in)
Select.Item은 Dropdown 정책에 따라 wrap이 디폴트입니다. 좁은 popup에서 매우 긴 라벨이 들어와도 가로 스크롤이 생기지 않고 자연스럽게 줄바꿈됩니다.
한 줄 ellipsis가 필요하면 Select.Item에 $css로 opt-in합니다:
<Select.Root defaultValue="long"> <Select.Trigger $css={{ width: '220px' }}> <Select.Value /> </Select.Trigger> <Select.Portal> <Select.Positioner> <Select.Popup> <Select.Item value="long" $css={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}> <Select.ItemText>매우 긴 옵션 라벨 — 한 줄 ellipsis로 잘립니다</Select.ItemText> </Select.Item> <Select.Item value="wrap"> <Select.ItemText>매우 긴 옵션 라벨 — 기본은 wrap이라 줄바꿈됩니다</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 가이드 →
Base UI Select를 래핑합니다. 여기에 명시되지 않은 props는 Base UI Select API를 참고하세요.
Select.Root
상태 관리 wrapper입니다. DOM 노드를 렌더링하지 않습니다.
extends Base UI Select.Root
Prop
Type
Select.Trigger
드롭다운을 여는 버튼입니다. Select.Indicator(ChevronDown)가 자동으로 children 끝에 렌더됩니다. <Select.Indicator>를 직접 배치하면 자동 렌더가 생략됩니다.
extends Base UI Select.Trigger
Prop
Type
Select.Value
선택된 값을 표시하는 영역입니다. overflow: hidden이 기본 적용됩니다.
extends Base UI Select.Value
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입니다.
extends Base UI Select.Positioner
Prop
Type
Select.Popup
드롭다운 팝업 컨테이너입니다. 기본 너비가 Trigger 너비(--anchor-width)에 맞춰집니다.
extends Base UI Select.Popup
Select.Item
개별 선택 항목입니다.
extends Base UI Select.Item
Prop
Type
Select.ItemCheckbox
다중 선택 시 checkbox indicator로 사용합니다. pointerEvents: 'none'이 자동 적용되어 Select의 클릭 이벤트를 방해하지 않습니다. disabled는 부모 Select.Item에서 context로 전파됩니다 — 직접 prop으로 override 가능 (own > ctx).
Prop
Type
Select.ItemRadio
단일 선택 시 radio indicator로 사용합니다. pointerEvents: 'none'이 자동 적용됩니다. disabled는 부모 Select.Item에서 context로 전파됩니다 (own > ctx).
Prop
Type
Select.ItemText
항목 텍스트 슬롯입니다. flexGrow: 1, minWidth: 0이 기본 적용되어 $css로 ellipsis를 opt-in할 수 있습니다.
extends Base UI Select.ItemText
Select.Group / Select.GroupLabel
연관된 항목을 묶는 컨테이너와 레이블입니다. Select.Group 안에 Select.GroupLabel을 배치합니다.
extends Base UI Select.Group / Base UI Select.GroupLabel
Select.Separator
항목 그룹 사이의 구분선입니다. size에 따라 수직 margin이 자동 조절됩니다.
extends Base UI Select.Separator
Select.Action
값에 참여하지 않는 시각적 항목입니다 (예: "전체 선택" 버튼). role="presentation"으로 렌더됩니다. 내부적으로 Dropdown.Item을 위임해 일반 row 스타일을 사용합니다.
Prop
Type
Select.Button
Button Contrast 스타일의 CTA 항목입니다. Popup 하단의 확인/취소 버튼 등에 사용합니다. 내부적으로 Dropdown.Action을 위임합니다.
Prop
Type
Select.Overflow
다중 선택 시 Value에서 overflow로 숨겨진 항목 수를 자동 감지합니다. Value 바깥, Trigger 안에 배치하세요.
Prop
Type
스타일
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 |
Transitions & Animation
- Size transition — Trigger와 Item에
min-height,padding,font-size등 size 관련 속성에150ms easetransition이 적용됩니다. 반응형size사용 시 브레이크포인트 전환이 부드럽게 동작합니다. - Indicator rotation — 기본 ChevronDown 아이콘은 popup open 시
transform: rotate(180deg)(150ms ease)로 회전합니다.
State Types
Prop
Type