Featuring Design System
ComponentsSelect

Select (Single)

드롭다운 목록에서 하나의 값을 선택하는 컴포넌트.

개요

Select드롭다운 목록에서 값을 선택하는 compound component입니다.

  • Compound compositionRoot, Trigger, Value, Icon, Indicator, Positioner, Popup, Item, ItemText, Group, GroupLabel, Separator, Action, Overflow
  • Base UI 기반 — Floating UI 포지셔닝, collision detection, 키보드 내비게이션 자동 처리
  • 3가지 sizesm (28px), md (32px, 기본), lg (40px)
  • 반응형 size{ mobile: 'sm', tablet: 'md' } 객체 지원
  • Field 연동Field.Root 안에 배치 시 status, disabled, readOnly 자동 전파
  • Indicator 자동 렌더 — Trigger에 ChevronDown이 자동 포함, <Select.Indicator>로 override 가능
  • $css prop — 모든 서브컴포넌트에서 rainbow-sprinkles 토큰 기반 스타일링
  • render prop — 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.2role="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

Sizemin-heightTypographyIcon 크기
sm28pxbody-116px
md32pxbody-216px
lg40pxbody-220px

Trigger States

StateBorderBackground
defaultborder-1background-1
hoverborder-2background-2
focus / openprimary-50background-1
readOnlyborder-2background-4
disabledborder-2background-4
errorsupport-error-3background-1
warningsupport-warning-3background-1
successsupport-success-3background-1

Item States

StateBackgroundColor
defaulttransparenttext-1
highlightedprimary-10primary-100
disabledtoggle-disabled-bgtoggle-disabled-text

State Types

Prop

Type