Featuring Design System
ComponentsSelect

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 compositionRoot, Trigger, Value, Icon, Indicator, Portal, Backdrop, Positioner, Popup, List, Item, ItemCheckbox, ItemRadio, ItemText, Group, GroupLabel, Separator, Action, Button, Overflow, Arrow
  • 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 — 모든 서브컴포넌트에서 디자인 토큰 기반 스타일링
  • 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가 자동으로 끝에 추가합니다.

언제 사용하나요

  • 5개 이상의 옵션에서 하나를 선택할 때
  • 폼에서 카테고리, 상태, 필터 같은 단일 값을 입력받을 때
  • 공간이 제한적이어서 Radio 목록을 펼칠 수 없을 때

언제 사용하면 안 되나요

  • 2~4개의 옵션Radio 또는 SegmentedControl 사용
  • 자유 텍스트 입력 + 검색 → Combobox (향후 제공)
  • 네비게이션 메뉴 → Dropdown Menu
  • on/off 토글Switch
  • 여러 개 동시 선택Select (Multiple)

접근성

키보드 동작

동작
Enter / SpaceTrigger 포커스 시 popup 열기
ArrowUp / ArrowDown항목 간 이동
Home / End첫 번째 / 마지막 항목으로 이동
Enter강조된 항목 선택 후 popup 닫기
Escapepopup 닫기, Trigger로 포커스 복귀
문자 입력해당 문자로 시작하는 항목으로 이동

스크린리더 동작

Base UI가 role="listbox", aria-expanded, aria-selected, aria-activedescendant를 자동으로 관리합니다. Select.Itemlabel prop으로 키보드 검색 텍스트를 별도 지정할 수 있습니다.

WCAG

기준적용
1.3.1 Info and Relationshipsrole="listbox" + aria-selected
2.1.1 Keyboard전체 키보드 동작 지원
2.4.7 Focus Visiblefocus-visibleprimary-50 아웃라인
4.1.2 Name, Role, Valuearia-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.sizeTrigger / 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에서 닫힐 때 키워드를 초기화합니다. PositionerdisableAnchorTracking을 추가해야 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.ValuechildrenTag를 렌더링하면 선택 값을 태그 형태로 표시할 수 있습니다.

() => {
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.Actionvalue 없이 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.Positionerside / 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.ItemDropdown 정책에 따라 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

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

Transitions & Animation

  • Size transition — Trigger와 Item에 min-height, padding, font-size 등 size 관련 속성에 150ms ease transition이 적용됩니다. 반응형 size 사용 시 브레이크포인트 전환이 부드럽게 동작합니다.
  • Indicator rotation — 기본 ChevronDown 아이콘은 popup open 시 transform: rotate(180deg) (150ms ease)로 회전합니다.

State Types

Prop

Type