ComponentsP3 본문

Select

커스텀 리스트박스 — APG select-only combobox 패턴. 포커스는 트리거에 머물고 aria-activedescendant가 활성 옵션을 가리킵니다. 제어(value)·비제어(defaultValue) 모두 지원, 옵션 그룹 헤더 지원.

마지막 업데이트 2026-06-11

한눈에#

커스텀 리스트박스 — APG select-only combobox. 고정 옵션에서 하나를 고르는 폼 값에 씁니다.

  • 단일 선택
  • 옵션 그룹
  • 타이프어헤드
  • APG combobox

기본 — 클릭 또는 ↓로 열고, 타이핑으로 점프

사용 시점#

고정 옵션 단일 선택이면 Select — 그 외에는 목적에 맞는 컴포넌트를 가리킵니다.

쓴다

고정 옵션에서 하나만 고르는 폼 값(환경·요금제·국가)

대신 MultiSelect

여러 값을 동시에 선택

대신 Autocomplete

서버 조회·자유 입력·비동기 제안

대신 RadioGroup

옵션 2~5개를 한눈에 보여야 할 때

열린 상태#

포털로 렌더되는 팝업은 정적 문서에 inline으로 못 띄우므로, faux-viewport에 열린 리스트박스를 재현합니다.

스테이징

개발
스테이징
프로덕션
  • 리스트박스 팝업
  • 활성 옵션 강조
  • 그룹 헤더
  • 타이프어헤드
열린 상태 — 트리거 + 리스트박스(스테이징 선택)

해부#

닫힌 트리거 위에 토큰 실측을 핀으로 얹습니다 — Select는 Input의 component 토큰(input.*)을 공유해 같은 줄에서 자동 정렬됩니다.

md 트리거 — input.* 토큰 실측 핀
height
44pxinput.height
padding-inline
12pxspace-3
gap
8pxspace-2
radius
10pxinput.radius
chevron
20pxicon.size-md
border
1pxinput.border
font
16pxbody-1

플레이그라운드#

컨트롤로 props를 조작하면 미리보기와 코드가 실시간 갱신됩니다.

import { Select } from '@wds/ui-web';

<Select
  aria-label="우선순위"
  options={[
    { value: 'low', label: '낮음' },
    { value: 'medium', label: '보통' },
    { value: 'high', label: '높음' },
  ]}
/>

변형#

defaultValue · 비활성 옵션(키보드 탐색이 건너뜀)

팝업은 트리거 아래에 뜨고, 뷰포트 하단 공간이 부족하면 위로 플립됩니다.

옵션 그룹#

옵션에 group을 주면 같은 group을 가진 연속 옵션 앞에 비대화 헤더가 한 번 끼워집니다. 헤더는 role="presentation" + aria-hidden이라 listbox 의미론에서 제외되고, 키보드 탐색·aria-activedescendant는 헤더를 건너뜁니다 (M3 ExposedDropdownMenu / Apple Menu 섹션 참조). group이 없는 옵션은 헤더 없이 평면으로 렌더됩니다.

group으로 묶은 섹션 — 헤더는 비대화(키보드가 건너뜀)

제어 · 비제어#

value를 넘기지 않으면 비제어defaultValue로 시작해 내부 상태가 선택을 소유합니다. value를 넘기면 제어 — 그 값이 진실의 원천이 되고 선택은 내부 상태를 쓰지 않은 채 onChange(next)만 통지하므로, 부모가 value를 갱신해야 표시가 바뀝니다(같은 값 재선택은 통지하지 않음). 타이프어헤드·listbox·모바일 시트 동작은 두 모드에서 동일합니다.

function EnvPicker() {
  const [env, setEnv] = useState('dev');
  return (
    <Select
      aria-label="배포 환경"
      value={env}
      onChange={setEnv}
      options={[
        { value: 'dev', label: '개발' },
        { value: 'staging', label: '스테이징' },
        { value: 'prod', label: '프로덕션' },
      ]}
    />
  );
}

크기#

트리거 높이는 input.height(44px) 단일 + 최소 폭 14rem — Input과 같은 폼 컨트롤 줄맞춤입니다. 리스트는 최대 16rem 높이에서 스크롤됩니다.

상태#

Disabled
  • Focus — 트리거 :focus-visibleinput.border-focus 보더 + 18% 링
  • 활성 옵션color.surface-selected 배경(마우스 호버도 활성을 따라감)
  • 선택 옵션aria-selected + color.primary-text 잉크
  • Disabledinput.bg-disabled 배경 + 커서 차단

모바일#

DK 모바일 재활용 표준입니다 — mobile을 켜면 트리거 클릭 시 데스크톱 팝업 대신 BottomSheet가 열리고, 옵션 리스트는 role="listbox" 의미론을 유지한 채 시트 안에 렌더됩니다. 단일 선택이므로 항목을 누르는 즉시 확정되고 시트가 닫힙니다(임시 상태 없음). 시트 제목은 mobileTitle, 없으면 aria-labelplaceholder 순으로 채워집니다. 라이브 데모는 데스크톱 뷰포트라 코드로 안내합니다.

<Select
  mobile
  mobileTitle="배포 환경 선택"
  aria-label="배포 환경"
  placeholder="환경 선택"
  options={[
    { value: 'dev', label: '개발' },
    { value: 'staging', label: '스테이징' },
    { value: 'prod', label: '프로덕션' },
  ]}
  onChange={(value) => applyEnvironment(value)}
/>

닫힘 경로(배경 클릭·Escape·닫기 버튼·핸들 드래그 다운)와 포커스 복원은 BottomSheet 계약을 그대로 따릅니다 — 값 변경 없이 닫힙니다.

Props#

Prop타입기본값설명
optionsReadonlyArray<SelectOption>{ value, label, disabled?, group? } 배열 — group이 같은 연속 옵션 앞에 헤더
valuestring제어 선택 value — 제공하면 제어 모드(내부 상태 무시, 선택은 onChange만 통지)
defaultValuestring초기 선택 value — 이후 상태는 내부 소유(비제어, value 미제공 시)
onChange(value: string) => void선택 변경 콜백 — 같은 값 재선택은 발화하지 않음
placeholderstring'선택'선택 전 트리거에 표시되는 문구
mobilebooleanfalsetrue면 팝업 대신 BottomSheet로 옵션 리스트를 엽니다 (DK 모바일 표준)
mobileTitlestring시트 제목 — 기본 aria-label → placeholder
…restOmit<ButtonHTMLAttributes, 'onChange' | 'value' | 'defaultValue'>aria-label · disabled 등은 트리거 button으로 전달

접근성#

트리거가 role="combobox" + aria-haspopup="listbox", 팝업이 role="listbox"입니다. 포커스는 항상 트리거에 머물고 aria-activedescendant가 활성 옵션 id를 가리킵니다.

동작
/ / Enter / Space (닫힘)열기 — 선택(없으면 첫 활성) 옵션 활성
/ (열림)활성 옵션 이동 — 비활성 옵션 건너뜀
Home / End첫/마지막 활성 옵션으로 점프
Enter / Space활성 옵션 선택 + 닫기 + 트리거 포커스 유지
Escape값 변경 없이 닫기
Tab닫고 기본 포커스 이동
문자 입력타이프어헤드 — label 전방 일치 옵션으로 점프(0.5s 버퍼)

외부 클릭으로도 닫히며, 비활성 옵션은 aria-disabled로 보고되고 클릭·선택이 차단됩니다. 옵션 그룹 헤더는 role="presentation" + aria-hidden이라 보조기술의 옵션 카운트와 키보드 탐색에서 모두 제외됩니다(/가 헤더를 건너뜀).

토큰#

component 토큰 없이 Input의 component 토큰(input.*)과 semantic을 직접 소비합니다(신설 기준 §4 미충족).

속성토큰
트리거input.bg/fg/border/height/radius · placeholder input.placeholder
포커스input.border-focus + color.primary 18% 링
팝업color.surface-raised · radius.md · shadow.lg · z.dropdown
옵션 활성/선택color.surface-selected · 선택 잉크 color.primary-text · 눌림 color.surface-pressed
옵션 높이control.height-sm
그룹 헤더color.text-subtle · font.size-caption · font.weight-medium
비활성input.bg-disabled · color.text-placeholder
모션duration.fast + ease.out/standard