DropdownMenu
트리거 버튼 + 액션 메뉴(WAI-ARIA menu button 패턴) — 열리면 첫 항목으로 포커스 이동, 화살표 순환 탐색, Escape는 closing exit 뒤 트리거로 복귀. 비제어(defaultOpen).
마지막 업데이트 2026-07-01
한눈에#
트리거 버튼 + 실행 가능한 액션 메뉴(WAI-ARIA menu button)입니다 — 열리면 첫 항목으로 포커스, 화살표 순환 탐색, 선택 시 실행 후 닫힙니다.
- menu button 패턴
- 첫 항목 자동 포커스
- compact는 BottomSheet
- 선택 즉시 실행·닫힘
트리거를 눌러 화살표 탐색·선택 실행을 직접 확인하세요:
사용 시점#
트리거로 실행 가능한 액션 목록을 펼치면 DropdownMenu — 폼 값 선택·임의 패널·명령 팔레트는 다른 컴포넌트입니다.
권장 — 이렇게 쓰세요
지양 — 이러지 마세요
쓴다 — 이름 바꾸기·복제·삭제처럼 트리거로 펼쳐 고르면 즉시 실행되는 액션 메뉴(menu button)
대신 Select — 폼에 제출할 단일 값을 고르는 선택일 때
대신 Popover — 메뉴가 아닌 임의 콘텐츠 패널일 때
대신 Command — 검색 가능한 명령 팔레트(⌘K / Ctrl K)일 때
플레이그라운드#
컨트롤로 props를 조작하면 미리보기와 코드가 실시간 갱신됩니다.
변형#
- danger — 파괴적 액션 변형,
color.error-text잉크 - disabled — 네이티브
disabled— 포커스 탐색이 건너뜀
- group — 같은
group을 공유하는 연속 항목 앞에 비대화형 섹션 헤더(role="presentation")를 렌더 — 헤더는menuitem이 아니라 키보드 탐색에서 건너뜁니다 - leadingIcon — 라벨 앞 장식 아이콘(
aria-hidden) - trailingText — 라벨 우측 보조 텍스트(키보드 숏컷 등), muted·우측 정렬
- separator — 헤더 없는 구분선(
role="separator")으로 관련 액션 묶음을 시각 분리합니다. 키보드 탐색에서 건너뜁니다 - items — 하위 액션을 가진 항목은 서브메뉴 부모가 됩니다 — 우측에
›,ArrowRight·Enter로 진입하고ArrowLeft로 복귀합니다
모바일#
responsive="auto"가 기본입니다. compact 뷰포트(<600px)에서는 데스크톱 앵커 메뉴 대신
BottomSheet 안의 전체 폭 액션 리스트로 전환합니다. responsive="mobile"은 강제 시트,
responsive="desktop"은 compact에서도 앵커 메뉴를 유지합니다. mobile prop은
responsive="mobile"의 하위호환 alias입니다.
- 시트 내부 리스트 — BottomSheet가 이미 표면이므로 메뉴 본문은
border·border-radius·shadow 없이 시트 폭을 채웁니다 - 서브메뉴 — 모바일에서는 측면 flyout을 만들지 않고 시트 안에서 하위 단계로 이동하며 헤더의 뒤로가기 액션으로 돌아옵니다. 본문 리스트에
이전행을 추가하지 않습니다 - 선택 — leaf 항목을 누르면
onSelect(value)가 즉시 호출되고 시트가 닫힙니다. disabled 항목은 무시됩니다 - 제목 —
mobileTitle→ 텍스트trigger→triggerAriaLabel→메뉴순서로 시트 제목을 정합니다
크기#
단일 크기입니다 — 트리거 높이 control.height-md(44px), 데스크톱 메뉴 최소 폭 11rem.
모바일 시트 항목도 44px 이상 터치 타깃을 유지합니다.
상태#
- 열림 — 트리거
aria-expanded="true"+ 메뉴 렌더, 첫 활성 항목 포커스 - 닫힘 전환 — 선택·Escape·외부 클릭·Tab 닫힘은 메뉴를 즉시 제거하지 않고
closing상태로 유지한 뒤, anchor-origin fade/scale-out이 끝나면 unmount - 항목 hover/focus —
color.surface-hover배경 (트리거 열림 상태만surface-selected) - Focus — 트리거
:focus-visible에 2pxcolor.focus-ring아웃라인
선택하면 onSelect?.(value) 호출 후 메뉴가 닫힘 전환을 재생하고 트리거로
포커스가 복귀합니다. 닫힘 중 메뉴는 pointer-events: none으로 후속 입력을 받지 않습니다.
Props#
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
trigger | ReactNode | — | 트리거 콘텐츠 — 내부에서 button으로 감싼다(자체 button 전달 금지) |
items | readonly DropdownMenuItem[] | — | { value, label, disabled?, danger?, group?, leadingIcon?, trailingText? } 배열 — 데이터 prop 방식 |
triggerVariant | 'default' | 'icon' | 'ghost' | default | 트리거 외형 — 'default'(secondary 버튼+chevron) · 'icon'(ghost 정사각 아이콘 버튼·chevron 없음) · 'ghost'(무테 컴팩트 텍스트+chevron — 컴포저 모델 선택 등 인라인 어포던스). 'icon'은 [triggerAriaLabel] 필수(아이콘만으론 의미 전달 불가). |
triggerSize | 'sm' | 'md' | md | 트리거 크기 — 'md'(기본) · 'sm'(컴팩트, 컴포저 액션 정렬용). icon/ghost 트리거에 적용. |
triggerShape | 'square' | 'round' | square | 트리거 모양 — 'square'(기본) · 'round'(완전 원형). icon 트리거를 IconButton round와 맞출 때. |
triggerAriaLabel | string | — | 트리거 버튼 접근성 레이블 — 아이콘 트리거(또는 콘텐츠가 텍스트 아닐 때)의 접근 가능 이름. |
onSelect | (value: string) => void | — | 항목 선택 콜백 — 호출 후 메뉴는 닫힌다 |
open | boolean | — | 제어 열림 상태 — 제공하면 제어 모드(내부 상태 무시, 모든 전환이 onOpenChange로 통지) |
onOpenChange | (open: boolean) => void | — | 열림/닫힘 전환 통지 — 제어/비제어 모두에서 호출 |
defaultOpen | boolean | false | 초기 열림 여부 — 이후 상태는 내부 소유(비제어, open 미제공 시) |
responsive | 'auto' | 'mobile' | 'desktop' | — | 적응형 표시 — auto(기본): compact 뷰포트에서 borderless/full-width BottomSheet 메뉴로 전환. mobile/desktop은 강제 override |
mobile | boolean | false | @deprecated responsive='mobile' 사용. true면 BottomSheet 메뉴 강제 — 하위호환 alias |
mobileTitle | ReactNode | — | 모바일 시트 제목 — 기본: 텍스트 trigger → triggerAriaLabel → '메뉴' |
className | string | — | 루트(div)에 적용 |
ref | Ref<HTMLButtonElement> | — | 트리거 <button>으로 (병합) 전달되는 ref (React 19 ref-as-prop) |
DropdownMenuItem#
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
value | string | — | 선택 시 onSelect로 전달되는 식별자 |
label | ReactNode | — | 항목 라벨 — menuitem 접근명 |
disabled | boolean | — | 네이티브 disabled — 선택·포커스 탐색에서 제외 |
danger | boolean | — | 파괴적 액션 변형 — error 잉크 |
group | string | — | 섹션 그룹 — 같은 group을 공유하는 연속 항목 앞에 비대화형 섹션 헤더를 렌더 |
leadingIcon | ReactNode | — | 라벨 앞 아이콘 — 장식(헤더/숏컷처럼 별도 ARIA 없음) |
trailingText | string | — | 라벨 우측 보조 텍스트 — 예: 키보드 숏컷. muted·우측 정렬 |
separator | boolean | — | 헤더 없는 구분선 — true면 label 대신 divider만 렌더(키 용도로 value는 필요). 키보드 탐색 제외. |
items | readonly DropdownMenuItem[] | — | 서브메뉴 — 있으면 이 항목은 서브메뉴 부모(우측에 자식 메뉴 펼침, › 표시·ArrowRight 진입). |
접근성#
데스크톱에서는 트리거가 aria-haspopup="menu" + aria-expanded + aria-controls,
팝업이 role="menu"(트리거 aria-labelledby 연결) + role="menuitem" 항목입니다.
모바일 시트에서는 트리거가 aria-haspopup="dialog"를 쓰고, BottomSheet가
role="dialog"/aria-modal/포커스 트랩/Escape/배경 클릭 닫기를 소유합니다.
group 섹션 헤더는 role="presentation"이라 menuitem이 아니며, 화살표·Home/End
탐색에서 자연히 건너뜁니다. leadingIcon·trailingText는 aria-hidden이라 항목
접근명은 label만 사용됩니다.
| 키 | 동작 |
|---|---|
↓ (트리거) | 열고 첫 활성 항목으로 포커스 |
↑ (트리거) | 열고 마지막 활성 항목으로 포커스 |
↓ / ↑ (메뉴) | 항목 포커스 순환 이동 — 비활성 건너뜀 |
Home / End | 첫/마지막 활성 항목으로 점프 |
Enter / Space | 항목 선택 + 닫기 + 트리거 포커스 복귀 |
Escape | 선택 없이 닫기 + 트리거 포커스 복귀 |
Tab | 메뉴는 탭 순서에 끼지 않음 — 닫고 기본 이동 |
외부 클릭(mousedown)으로도 닫히며, 항목은 tabIndex={-1}로 화살표
탐색만 허용합니다(menu button 패턴).
토큰#
component 토큰 없이 semantic을 직접 소비합니다(신설 기준 §4 미충족).
| 속성 | 토큰 |
|---|---|
| 트리거 | color.surface + color.border (hover surface-hover/border-strong · pressed surface-pressed) |
| 트리거 높이/라운드 | control.height-md · radius.control |
| 메뉴 | color.surface-raised · radius.md · shadow.lg · z.dropdown |
| 항목 | font.size-body-2 · hover/focus color.surface-hover · pressed color.surface-pressed |
| danger | color.error-text (hover 배경 color.error 톤) |
| 비활성 | color.disabled-fg |
| 모션 | 등장 spring.effect.fast + ease.emphasized-decelerate, 닫힘 spring.effect.fast + ease.emphasized-accelerate, 상태 전환 ease.standard |