ComponentsP3 본문

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를 조작하면 미리보기와 코드가 실시간 갱신됩니다.

DropdownMenu를 직접 조작해 보세요

속성·테마·토큰을 바꾸고 React·Flutter 코드를 확인하는 풀스크린 빌더로 엽니다.

빌더 열기

플레이그라운드는 넓은 작업 영역이 필요해 웹·태블릿에서 편집할 수 있어요.

변형#

danger · disabled 항목
  • danger — 파괴적 액션 변형, color.error-text 잉크
  • disabled — 네이티브 disabled — 포커스 탐색이 건너뜀
섹션(group) · leadingIcon · trailingText — 같은 group 연속 항목 앞에 헤더
  • group — 같은 group을 공유하는 연속 항목 앞에 비대화형 섹션 헤더(role="presentation")를 렌더 — 헤더는 menuitem이 아니라 키보드 탐색에서 건너뜁니다
  • leadingIcon — 라벨 앞 장식 아이콘(aria-hidden)
  • trailingText — 라벨 우측 보조 텍스트(키보드 숏컷 등), muted·우측 정렬
구분선(separator) · 서브메뉴(items) — 묶음 분리와 중첩 액션
  • 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 → 텍스트 triggertriggerAriaLabel메뉴 순서로 시트 제목을 정합니다

크기#

단일 크기입니다 — 트리거 높이 control.height-md(44px), 데스크톱 메뉴 최소 폭 11rem. 모바일 시트 항목도 44px 이상 터치 타깃을 유지합니다.

상태#

  • 열림 — 트리거 aria-expanded="true" + 메뉴 렌더, 첫 활성 항목 포커스
  • 닫힘 전환 — 선택·Escape·외부 클릭·Tab 닫힘은 메뉴를 즉시 제거하지 않고 closing 상태로 유지한 뒤, anchor-origin fade/scale-out이 끝나면 unmount
  • 항목 hover/focuscolor.surface-hover 배경 (트리거 열림 상태만 surface-selected)
  • Focus — 트리거 :focus-visible에 2px color.focus-ring 아웃라인

선택하면 onSelect?.(value) 호출 후 메뉴가 닫힘 전환을 재생하고 트리거로 포커스가 복귀합니다. 닫힘 중 메뉴는 pointer-events: none으로 후속 입력을 받지 않습니다.

Props#

Prop타입기본값설명
triggerReactNode트리거 콘텐츠 — 내부에서 button으로 감싼다(자체 button 전달 금지)
itemsreadonly 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와 맞출 때.
triggerAriaLabelstring트리거 버튼 접근성 레이블 — 아이콘 트리거(또는 콘텐츠가 텍스트 아닐 때)의 접근 가능 이름.
onSelect(value: string) => void항목 선택 콜백 — 호출 후 메뉴는 닫힌다
openboolean제어 열림 상태 — 제공하면 제어 모드(내부 상태 무시, 모든 전환이 onOpenChange로 통지)
onOpenChange(open: boolean) => void열림/닫힘 전환 통지 — 제어/비제어 모두에서 호출
defaultOpenbooleanfalse초기 열림 여부 — 이후 상태는 내부 소유(비제어, open 미제공 시)
responsive'auto' | 'mobile' | 'desktop'적응형 표시 — auto(기본): compact 뷰포트에서 borderless/full-width BottomSheet 메뉴로 전환. mobile/desktop은 강제 override
mobilebooleanfalse@deprecated responsive='mobile' 사용. true면 BottomSheet 메뉴 강제 — 하위호환 alias
mobileTitleReactNode모바일 시트 제목 — 기본: 텍스트 trigger → triggerAriaLabel → '메뉴'
classNamestring루트(div)에 적용
refRef<HTMLButtonElement>트리거 <button>으로 (병합) 전달되는 ref (React 19 ref-as-prop)
Prop타입기본값설명
valuestring선택 시 onSelect로 전달되는 식별자
labelReactNode항목 라벨 — menuitem 접근명
disabledboolean네이티브 disabled — 선택·포커스 탐색에서 제외
dangerboolean파괴적 액션 변형 — error 잉크
groupstring섹션 그룹 — 같은 group을 공유하는 연속 항목 앞에 비대화형 섹션 헤더를 렌더
leadingIconReactNode라벨 앞 아이콘 — 장식(헤더/숏컷처럼 별도 ARIA 없음)
trailingTextstring라벨 우측 보조 텍스트 — 예: 키보드 숏컷. muted·우측 정렬
separatorboolean헤더 없는 구분선 — true면 label 대신 divider만 렌더(키 용도로 value는 필요). 키보드 탐색 제외.
itemsreadonly 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·trailingTextaria-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
dangercolor.error-text (hover 배경 color.error 톤)
비활성color.disabled-fg
모션등장 spring.effect.fast + ease.emphasized-decelerate, 닫힘 spring.effect.fast + ease.emphasized-accelerate, 상태 전환 ease.standard