Menu | CoUI

Menu

메뉴 및 컨텍스트 메뉴 컴포넌트

Menu#

액션 목록을 표시하는 메뉴 컴포넌트입니다. 드롭다운 및 컨텍스트 메뉴로 사용할 수 있습니다.

Live Preview#

Web
Flutter
Loading Flutter...
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#

속성타입기본값설명
triggerWidget필수메뉴를 여는 트리거 위젯
itemsList<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 + flat separatorColor / 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 텍스트
separatorColorColor?MenuDivider 색상
separatorThickness double? divider 두께 (logical px)
separatorIndentdouble?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)#

항목FlutterWeb
메뉴 타입 MenuButton, MenuCheckbox, MenuRadio, MenuLabel, MenuDivider, MenuGap MenuItem, MenuTitle, MenuSubmenu, MenuHoverSubmenu
서브메뉴 subMenu 속성으로 중첩 <details>/<summary> HTML 네이티브
컨텍스트 메뉴 onSecondaryTapDown + onLongPressStart contextmenu JS 이벤트
포지셔닝PopoverOverlay 뷰포트 자동 계산CSS absolute 위치
애니메이션100ms fade-in, AnimatedValueBuilderCSS transitions
키보드 Actions/Shortcuts + SubFocusScope HTML 네이티브 포커스
Surface 효과surfaceBlur, surfaceOpacityCSS 스타일링
크기_kMenuMinWidth = 192.0xs/sm/md/lg/xl Tailwind 크기
ARIA Flutter Semantics role="menu", role="menuitem"
테마MenuTheme, MenuPopupThemeCSS 클래스 (CoUI)
  • 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),
    ],
  ),
])