MultiSelect
다중 선택 콤보박스 — 칩 트리거 + aria-multiselectable 리스트박스. Enter/Space 토글은 목록을 닫지 않고, 포커스는 트리거(검색 시 검색 입력)에 머뭅니다. 비제어(defaultValue) 우선 (DK WizDropDown 사양 승격).
마지막 업데이트 2026-06-11
한눈에#
다중 선택 콤보박스 — 칩 트리거 + aria-multiselectable 리스트박스. 선택값은 트리거 안에 제거 버튼이 달린 칩으로 요약됩니다.
- 다중 선택
- 칩 트리거
- 검색 가능
- 비제어 우선
기본 — Enter/Space 토글(목록은 닫히지 않음)
사용 시점#
여러 값을 골라 칩으로 요약하면 MultiSelect — 그 외에는 목적에 맞는 컴포넌트를 가리킵니다.
고정 옵션에서 여러 값 동시 선택(담당자·태그·필터 조합)
플레이그라운드#
컨트롤로 props를 조작하면 미리보기와 코드가 실시간 갱신됩니다.
import { MultiSelect } from '@wds/ui-web';
<MultiSelect
aria-label="담당 에이전트"
options={[
{ value: 'frontend', label: '프론트엔드' },
{ value: 'backend', label: '백엔드' },
{ value: 'mobile', label: '모바일' },
{ value: 'data', label: '데이터' },
]}
/>변형#
showAll이면 목록 맨 위에 "전체" 토글 행이 생기고, 전체가 선택되면 트리거에
칩 나열 대신 전체 1개가 표시됩니다(그 칩의 ×는 전체 해제).
열리면 포커스가 검색 입력으로 이동합니다. 검색 중에는 "전체" 행이 숨겨집니다 — 필터된 부분집합에 대한 전체 토글은 의미가 모호하기 때문입니다.
그룹 일부만 선택되면 헤더 체크박스가 부분 선택(−)으로 표시됩니다. 비활성 옵션은 전체/그룹 토글에서도 제외됩니다.
크기#
단일 크기입니다 — 트리거는 input.height(44px)를 최소 높이로 칩이
줄바꿈되면 늘어나고, 최소 폭 16rem입니다. 리스트는 최대 16rem 높이에서
스크롤됩니다.
상태#
- Focus — 트리거
:focus-visible에input.border-focus보더 + 18% 링 - 활성 행 —
color.surface-hover배경(마우스 호버도 활성을 따라감), 활성+선택 행은color.surface-selected - 선택 행 —
aria-selected+color.primary-text잉크 + 체크 표시 - Loading — 목록 대신 Spinner / 빈 결과 —
emptyText(role="status") - Disabled —
input.bg-disabled배경 + 포커스·토글 차단
모바일#
DK 모바일 재활용 표준입니다 — mobile을 켜면 팝업 대신
BottomSheet 체크리스트가 열립니다.
데스크톱과 달리 임시 선택입니다: 체크를 바꿔도 즉시 확정되지 않고,
footer의 확인을 눌러야 onChange가 한 번 발화합니다. 취소·배경
클릭·Escape·핸들 드래그 다운은 임시 선택을 폐기하며, 시트는 열릴 때마다
커밋된 값으로 새로 시작합니다. searchable·showAll·groupSelectable은
시트 안에서도 동일하게 동작합니다. 라이브 데모는 데스크톱 뷰포트라 코드로
안내합니다.
<MultiSelect
mobile
mobileTitle="에이전트 선택"
confirmLabel="적용"
searchable
showAll
options={agentOptions}
onChange={(values) => applyAgents(values)}
/>
Props#
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
options | ReadonlyArray<MultiSelectOption> | — | 옵션 배열 (아래 표) — 데이터 prop 방식 |
defaultValue | ReadonlyArray<string> | — | 초기 선택 value 배열 — 이후 상태는 내부 소유(비제어) |
onChange | (values: string[]) => void | — | 선택 집합 변경 콜백 — 항상 새 배열로 통지 |
placeholder | string | '선택' | 선택이 없을 때 트리거에 표시되는 문구 |
maxVisibleTags | number | 3 | 트리거 칩 노출 상한 — 초과분은 "+N"으로 합산 표시 |
showAll | boolean | false | 목록 맨 위 "전체" 토글 행 — 전체 선택 시 트리거에 전체 1개 표시 |
allLabel | string | '전체' | 전체 행/칩의 라벨 |
searchable | boolean | false | 팝업 상단 검색 입력 — label 부분일치(대소문자 무시) |
onSearchChange | (query: string) => void | — | 검색어 변경 통지 — 서버 필터 연동용 |
searchPlaceholder | string | '검색' | 검색 입력 placeholder |
searchLabel | string | '옵션 검색' | 검색 입력 aria-label |
groupSelectable | boolean | false | 그룹 헤더를 체크박스 행으로 — 전체/부분(indeterminate) 토글 |
allowClear | boolean | false | 트리거 우측 전체 비우기 × 버튼 |
clearLabel | string | '모두 지우기' | 비우기 버튼 aria-label |
loading | boolean | false | 목록 대신 로딩(Spinner) 표시 |
emptyText | string | '결과 없음' | 옵션/검색 결과 0건 표시 문구 |
mobile | boolean | false | true면 BottomSheet 체크리스트 — 임시 선택 후 확인으로 확정 (DK 모바일 표준) |
mobileTitle | ReactNode | '선택' | 시트 제목 |
confirmLabel | string | '확인' | mobile 확정 버튼 라벨 |
cancelLabel | string | '취소' | mobile 취소 버튼 라벨 |
disabled | boolean | false | 비활성 — 열기·토글·포커스 차단 |
…rest | Omit<HTMLAttributes<HTMLDivElement>, 'onChange' | 'defaultValue'> | — | aria-label 등은 트리거 div(role=combobox)로 전달 |
MultiSelectOption:
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
value | string | — | 고유 value — 선택 집합의 원소 |
label | string | — | 표시·검색 대상 라벨 |
disabled | boolean | false | 선택 차단 — 전체/그룹 토글에서도 제외 |
group | string | — | 같은 group 문자열의 연속 옵션은 그룹 헤더 아래 묶임 |
접근성#
트리거가 role="combobox" + aria-haspopup="listbox"(mobile은 "dialog")인
div입니다 — 칩 제거 버튼이 트리거 안에 살기 때문에 중첩 버튼을 피해
div + tabIndex=0으로 구현됩니다. 팝업은 ul[role="listbox"] +
aria-multiselectable="true"이고, 포커스는 트리거(searchable이면 검색
입력)에 머물며 aria-activedescendant가 활성 행 id를 가리킵니다.
| 키 | 동작 |
|---|---|
↓ / ↑ / Enter / Space (닫힘) | 열기 — 첫 탐색 가능 행 활성 |
↓ / ↑ (열림) | 활성 행 이동 — 비활성 옵션 건너뜀 |
Home / End | 첫/마지막 행으로 점프 (검색 입력에서는 텍스트 편집에 양보) |
Enter / Space | 활성 행 토글 — 목록 유지 (검색 입력의 Space는 텍스트 입력) |
Escape | 닫기 + 트리거 포커스 복원 |
Tab | 닫고 기본 포커스 이동 |
- 행 선택 상태는
aria-selected, 부분 선택(전체/그룹)은data-indeterminate— 시각 체크박스는aria-hidden - 비활성 옵션은
aria-disabled로 보고되고 클릭·키보드 토글이 차단됩니다 - 외부 클릭으로 닫히며, 옵션 클릭은
mousedown기본 동작 차단으로 트리거/검색 입력 포커스를 유지합니다 - 뷰포트 하단 공간이 부족하면 팝업이 위로 플립됩니다 (Select 패턴)
토큰#
component 토큰 없이 Input·Checkbox의 component 토큰과 semantic을 직접 소비합니다(신설 기준 §4 미충족).
| 속성 | 토큰 |
|---|---|
| 트리거 | input.bg/fg/border/radius · 최소 높이 input.height · placeholder input.placeholder |
| 포커스 | input.border-focus + color.primary 18% 링 |
| 칩 초과(+N) | color.surface-muted · color.text-muted · radius.full |
| 팝업 | color.surface-raised · radius.md · shadow.lg · z.dropdown |
| 행 활성/선택 | color.surface-hover · 선택 잉크 color.primary-text · 활성+선택 color.surface-selected · 눌림 color.surface-pressed |
| 행 체크박스 | checkbox.bg/border/radius/check/bg-checked (Checkbox 어휘 공유) |
| 행 높이 | control.height-sm — 모바일 시트 행은 control.height-md |
| 비활성 | input.bg-disabled · color.text-placeholder |
| 모션 | duration.fast + ease.standard/out |