Menu#
액션 목록을 표시하는 메뉴 컴포넌트입니다. 드롭다운 및 컨텍스트 메뉴로 사용할 수 있습니다.
Live Preview#
Menu([
MenuTitle([text('Navigation')]),
MenuItem([text('Home')]),
MenuItem([text('About')], isActive: true),
MenuItem([text('Contact')]),
])
MenuGroup(
direction: Axis.vertical,
children: [
MenuLabel(child: Text('Navigation')),
MenuButton(onPressed: () {}, child: Text('Home')),
MenuButton(onPressed: () {}, child: Text('About')),
MenuButton(onPressed: () {}, child: Text('Contact')),
],
builder: (context, children) {
return MenuPopup(children: children);
},
)
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- 버튼이나 아이콘에 연결된 액션 목록을 드롭다운으로 보여줄 때
- 우클릭/롱프레스로 컨텍스트 메뉴를 제공할 때
- 서브메뉴를 포함한 계층적 액션 구조가 필요할 때
대신 다른 컴포넌트를 사용하세요:
Select: 폼에서 값을 선택하는 드롭다운일 때Command: 검색 기반 액션 선택이 필요할 때Popover: 임의의 콘텐츠를 팝업으로 보여줄 때
기본 사용법 (Basic Usage)#
// 드롭다운 메뉴
Menu(
trigger: Button(
onPressed: null,
child: Text('메뉴'),
),
items: [
MenuItem(
label: '편집',
icon: Icons.edit,
onTap: handleEdit,
),
MenuItem(
label: '복사',
icon: Icons.copy,
onTap: handleCopy,
),
MenuDivider(),
MenuItem(
label: '삭제',
icon: Icons.delete,
onTap: handleDelete,
destructive: true,
),
],
)
// 컨텍스트 메뉴
ContextMenu(
items: menuItems,
child: Text('우클릭하세요'),
)
Menu(
trigger: Button(onClick: null, child: Text('메뉴')),
items: [
MenuItem(label: '편집', icon: Icons.edit, onClick: handleEdit),
MenuItem(label: '복사', icon: Icons.copy, onClick: handleCopy),
MenuDivider(),
MenuItem(label: '삭제', icon: Icons.delete, onClick: handleDelete),
],
)
Props / Parameters#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
trigger | Widget | 필수 | 메뉴를 여는 트리거 위젯 |
items | List<MenuEntry> | 필수 | 메뉴 항목 목록 |
placement |
MenuPlacement |
bottomStart |
메뉴 표시 위치 |
onOpen |
VoidCallback? |
null |
메뉴 열림 콜백 |
onClose |
VoidCallback? |
null |
메뉴 닫힘 콜백 |
dropdownMenuStyle |
CoreDropdownMenuStyle<Color, List<BoxShadow>>?
(Flutter) /
CoreDropdownMenuStyle<String, String>?
(Web)
|
null |
인스턴스 스타일 (Style 시스템 참조) |
스타일 시스템 (Style System)#
DropdownMenu / Menu / Menubar / ContextMenu 의 모든 chrome / dimensional / nested-slot 오버라이드는 공통
CoreDropdownMenuStyle<Clr, Shadow> 단일 슬롯으로 흐릅니다 (Epic #1302 원칙 6/7/8). 4 종류 위젯이지만 chrome 은 모두 동일 Style 클래스로 받습니다.
시맨틱 vs 스타일#
-
시맨틱 / behaviour: 위젯 파라미터로 직접 (
autofocus,enabled,placement…) -
chrome / dimensional / 슬롯 스타일:
CoreDropdownMenuStyle한 곳으로 (popoverStyle/triggerStyle/itemStyle/headerTextStyle+ flatseparatorColor/separatorThickness/separatorIndent) - 변형 교체: asChild — 트리거를 직접 위젯으로 주입
Resolve chain#
design system default for menu
→ CoreDropdownMenuTheme.style // 프로젝트 공통
→ parent component slot override
→ widget.dropdownMenuStyle // 인스턴스별
슬롯 매핑 (CoreDropdownMenuStyle 7 필드)#
| 필드 | 타입 (Flutter) | 적용 영역 |
|---|---|---|
popoverStyle |
CorePopoverStyle<Color, List<BoxShadow>>? |
팝오버 패널 chrome (border / radius / padding / shadow / blur / open animation) |
triggerStyle |
CoreButtonStyle<Color>? |
위젯이 자체 트리거를 렌더링할 때 |
itemStyle |
CoreButtonStyle<Color>? |
각 메뉴 아이템 (MenuButton / MenuItem) chrome |
headerTextStyle |
CoreTextStyle<Color>? |
MenuLabel / MenuTitle 텍스트 |
separatorColor | Color? | MenuDivider 색상 |
separatorThickness |
double? |
divider 두께 (logical px) |
separatorIndent | double? | divider 좌우 indent |
Migration — 옛 평면 필드 → 새 위치 매핑#
| 옛 chrome 필드 | 새 위치 |
|---|---|
MenuPopupTheme.borderColor |
dropdownMenuStyle.popoverStyle.panelBorderColor |
MenuPopupTheme.borderRadius |
dropdownMenuStyle.popoverStyle.panelBorderRadius (logical px) |
MenuPopupTheme.fillColor |
dropdownMenuStyle.popoverStyle.panelBackgroundColor |
MenuPopupTheme.padding |
dropdownMenuStyle.popoverStyle.panelPadding |
MenuTheme.itemPadding |
dropdownMenuStyle.itemStyle.paddingH |
사용 예 (Flutter)#
MenuPopup(
dropdownMenuStyle: CoreDropdownMenuStyle(
popoverStyle: CorePopoverStyle(
panelBorderRadius: 12,
panelBorderColor: Color(0xFFE5E7EB),
panelPadding: 8,
),
),
children: [
MenuButton(child: Text('Item 1')),
MenuButton(child: Text('Item 2')),
],
)
사용 예 (Web)#
Menu(
[
MenuItem([Component.text('Item 1')]),
MenuItem([Component.text('Item 2')]),
],
dropdownMenuStyle: CoreDropdownMenuStyle<String, String>(
popoverStyle: CorePopoverStyle<String, String>(
panelBorderRadius: 12,
),
),
)
변형 (Variants)#
서브메뉴#
Menu(
trigger: trigger,
items: [
MenuItem(label: '파일', onTap: handleFile),
SubMenu(
label: '내보내기',
items: [
MenuItem(label: 'PDF', onTap: handleExportPdf),
MenuItem(label: 'CSV', onTap: handleExportCsv),
],
),
],
)
단축키 표시#
MenuItem(
label: '붙여넣기',
icon: Icons.paste,
shortcut: 'Ctrl+V',
onTap: handlePaste,
)
체크 메뉴#
MenuCheckItem(
label: '자동 저장',
checked: isAutoSave,
onChanged: handleAutoSaveToggle,
)
동작 스펙 (Behavior)#
열기/닫기#
- 드롭다운 메뉴: 트리거 클릭으로 열기, 항목 선택 또는 외부 클릭으로 닫기
- 컨텍스트 메뉴: 우클릭(데스크톱) 또는 롱프레스(모바일)로 열기
- Menubar: 호버로 드롭다운 전환 (이미 열린 상태에서)
서브메뉴#
SubMenu(
label: '내보내기',
items: [
MenuItem(label: 'PDF', onTap: handleExportPdf),
MenuItem(label: 'CSV', onTap: handleExportCsv),
],
)
- Flutter: 화살표 키(→)로 서브메뉴 열기, (←)로 닫기. PopoverOverlay로 위치 자동 계산
- Web:
<details>/<summary>또는 CSS 호버로 펼침
애니메이션#
- Flutter: 100ms fade-in 트랜지션. 컨텍스트 메뉴는 커서 위치에서 등장
- Web: CSS transition (CoUI/Tailwind 기반)
자동 닫기#
autoClose: true(기본): 항목 선택 시 메뉴 자동 닫힘- 체크/라디오 항목은
autoClose: false로 여러 번 토글 가능
Surface 효과#
MenuPopupTheme(
surfaceBlur: 10.0,
surfaceOpacity: 0.8,
)
글래스모피즘 배경의 메뉴 팝업을 구현할 수 있습니다.
사용 가이드라인 (Usage Guidelines)#
✅ Do#
위험한 액션은 구분선과 destructive 스타일로 분리하세요.
Menu(
trigger: trigger,
items: [
MenuItem(label: '편집', onTap: handleEdit),
MenuItem(label: '복사', onTap: handleCopy),
MenuDivider(),
MenuItem(label: '삭제', destructive: true, onTap: handleDelete),
],
)
실수로 위험한 액션을 선택하는 것을 방지합니다.
❌ Don't#
위험한 액션을 일반 항목과 섞지 마세요.
Menu(
trigger: trigger,
items: [
MenuItem(label: '편집', onTap: handleEdit),
MenuItem(label: '삭제', onTap: handleDelete),
MenuItem(label: '복사', onTap: handleCopy),
],
)
삭제가 중간에 있으면 실수로 클릭할 위험이 높아집니다.
✅ Do#
메뉴 항목에 아이콘과 단축키를 함께 표시하세요.
MenuItem(
label: '복사',
icon: Icons.copy,
shortcut: '⌘C',
onTap: handleCopy,
)
시각적 힌트로 빠르게 찾고, 단축키를 학습할 수 있습니다.
❌ Don't#
10개 이상의 항목을 한 단계에 나열하지 마세요.
Menu(
trigger: trigger,
items: List.generate(15, (i) => MenuItem(label: '항목 $i')),
)
항목이 많으면 서브메뉴로 그룹화하거나 Command 팔레트를 사용하세요.
✅ Do#
비활성 항목은 왜 비활성인지 힌트를 제공하세요.
MenuItem(
label: '붙여넣기',
icon: Icons.paste,
enabled: hasClipboard,
onTap: handlePaste,
)
클립보드가 비어있으면 자연스럽게 비활성화됩니다.
❌ Don't#
사용 불가능한 항목을 완전히 숨기지 마세요.
사용자가 기능의 존재를 알 수 없고, 메뉴 구조가 매번 변해 혼란스럽습니다.
접근성 (Accessibility)#
키보드 인터랙션#
| 키 | 동작 |
|---|---|
↑ / ↓ | 메뉴 항목 간 이동 (세로 메뉴) |
← / → | 서브메뉴 열기/닫기, 메뉴바 항목 이동 |
Enter / Space | 포커스된 항목 실행 |
Escape | 메뉴 닫기 (서브메뉴 → 부모 메뉴로 이동) |
Tab | 다음 포커스 가능 요소로 이동 |
스크린 리더#
- Flutter:
SubFocusScope로 포커스 관리. 메뉴 항목의 라벨이 읽힘 -
Web:
role="menu"(세로) /role="menubar"(가로),role="menuitem"(각 항목),aria-orientation으로 방향 전달
터치 타겟#
- 메뉴 항목 최소 높이: 48dp
- 컨텍스트 메뉴: 롱프레스(모바일) 또는 우클릭(데스크톱)으로 열기
크로스 플랫폼 차이점 (Platform Differences)#
| 항목 | Flutter | Web |
|---|---|---|
| 메뉴 타입 | MenuButton, MenuCheckbox, MenuRadio, MenuLabel, MenuDivider, MenuGap | MenuItem, MenuTitle, MenuSubmenu, MenuHoverSubmenu |
| 서브메뉴 | subMenu 속성으로 중첩 |
<details>/<summary> HTML 네이티브 |
| 컨텍스트 메뉴 | onSecondaryTapDown + onLongPressStart |
contextmenu JS 이벤트 |
| 포지셔닝 | PopoverOverlay 뷰포트 자동 계산 | CSS absolute 위치 |
| 애니메이션 | 100ms fade-in, AnimatedValueBuilder | CSS transitions |
| 키보드 | Actions/Shortcuts + SubFocusScope |
HTML 네이티브 포커스 |
| Surface 효과 | surfaceBlur, surfaceOpacity | CSS 스타일링 |
| 크기 | _kMenuMinWidth = 192.0 | xs/sm/md/lg/xl Tailwind 크기 |
| ARIA | Flutter Semantics | role="menu", role="menuitem" |
| 테마 | MenuTheme, MenuPopupTheme | CSS 클래스 (CoUI) |
관련 컴포넌트 (Related Components)#
- Popover: 임의의 콘텐츠 팝업. Menu는 Popover 위에 구축된 액션 특화 컴포넌트
- Command: 검색 기반 액션 팔레트. 항목이 많을 때 Menu 대신 사용
- Select: 폼 값 선택 드롭다운. Menu는 액션용, Select는 값 선택용
조합 예제#
// 파일 관리 메뉴바 패턴
Menubar(children: [
MenuButton(
child: Text('파일'),
subMenu: [
MenuButton(child: Text('새 파일'), leading: Icon(Icons.add), onPressed: handleNew),
MenuButton(child: Text('열기'), trailing: Text('⌘O'), onPressed: handleOpen),
MenuDivider(),
MenuButton(
child: Text('내보내기'),
subMenu: [
MenuButton(child: Text('PDF'), onPressed: handlePdf),
MenuButton(child: Text('CSV'), onPressed: handleCsv),
],
),
],
),
MenuButton(
child: Text('편집'),
subMenu: [
MenuButton(child: Text('복사'), trailing: Text('⌘C'), onPressed: handleCopy),
MenuButton(child: Text('붙여넣기'), trailing: Text('⌘V'), onPressed: handlePaste),
MenuDivider(),
MenuCheckbox(child: Text('자동 저장'), value: autoSave, onChanged: handleAutoSave),
],
),
])