Menubar | CoUI

Menubar

데스크탑 앱 스타일의 상단 가로 메뉴 바 컴포넌트

Menubar#

데스크탑 애플리케이션 스타일의 상단 가로 메뉴 바 컴포넌트입니다. 중첩 메뉴와 키보드 탐색을 지원합니다.

Live Preview#

Flutter
Loading Flutter...
Menubar([
  MenuItem([text('File')]),
  MenuItem([text('Edit')]),
  MenuItem([text('View')]),
])
Card(
  child: Row(
    children: [
      TextButton(onPressed: () {}, child: Text('File')),
      TextButton(onPressed: () {}, child: Text('Edit')),
      TextButton(onPressed: () {}, child: Text('View')),
    ],
  ),
)

사용 시기 (When to Use)#

이 컴포넌트를 사용하세요:

  • IDE, 문서 편집기, 데스크탑 앱처럼 파일/편집/보기 스타일의 메뉴 바가 필요할 때
  • 많은 기능을 계층적 메뉴 구조로 정리해야 할 때
  • 키보드 단축키와 함께 메뉴 항목을 표시할 때

대신 다른 컴포넌트를 사용하세요:

  • Navigation: 페이지 간 이동을 위한 네비게이션 메뉴에
  • DropdownMenu: 단일 버튼의 드롭다운 액션 목록에
  • Tabs: 동일 화면 내 콘텐츠 섹션 전환에

기본 사용법 (Basic Usage)#

// 기본 메뉴 바
Menubar(
  onSelect: handleMenubarSelect,
  items: [
    MenubarItem(
      label: '파일',
      menu: [
        MenubarMenu(value: 'new', label: '새 파일'),
        MenubarMenu(value: 'open', label: '열기'),
        MenubarSeparator(),
        MenubarMenu(value: 'save', label: '저장'),
        MenubarMenu(value: 'saveAs', label: '다른 이름으로 저장'),
        MenubarSeparator(),
        MenubarMenu(value: 'exit', label: '종료'),
      ],
    ),
    MenubarItem(
      label: '편집',
      menu: [
        MenubarMenu(value: 'undo', label: '실행 취소'),
        MenubarMenu(value: 'redo', label: '다시 실행'),
        MenubarSeparator(),
        MenubarMenu(value: 'cut', label: '잘라내기'),
        MenubarMenu(value: 'copy', label: '복사'),
        MenubarMenu(value: 'paste', label: '붙여넣기'),
      ],
    ),
    MenubarItem(
      label: '보기',
      menu: [
        MenubarMenu(value: 'zoomIn', label: '확대'),
        MenubarMenu(value: 'zoomOut', label: '축소'),
        MenubarMenu(value: 'resetZoom', label: '기본 배율'),
      ],
    ),
  ],
)
Menubar(
  onSelect: handleMenubarSelect,
  items: [
    MenubarItem(
      label: '파일',
      menu: [
        MenubarMenu(value: 'new', label: '새 파일'),
        MenubarMenu(value: 'open', label: '열기'),
        MenubarSeparator(),
        MenubarMenu(value: 'save', label: '저장'),
      ],
    ),
    MenubarItem(
      label: '편집',
      menu: [
        MenubarMenu(value: 'undo', label: '실행 취소'),
        MenubarMenu(value: 'redo', label: '다시 실행'),
      ],
    ),
  ],
)

Props / Parameters#

속성타입기본값설명
items List<MenubarItem> 필수 상단 메뉴 아이템 목록
onSelect void Function(String value)? 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: 위젯 파라미터로 직접 (border, popoverOffset …)
  • chrome / dimensional / 슬롯 스타일: CoreDropdownMenuStyle 한 곳으로 (popoverStyle / triggerStyle / itemStyle / headerTextStyle + flat separatorColor / separatorThickness / separatorIndent)
  • 변형 교체: asChild — 트리거를 직접 위젯으로 주입

Resolve chain#

design system default for menubar
  → CoreDropdownMenuTheme.style                  // 프로젝트 공통
  → parent component slot override
  → widget.dropdownMenuStyle                     // 인스턴스별

Menubar 의 dropdownMenuStyleData.maybeOf<MenubarState> 를 통해 자식 submenu 팝업으로도 자동 전파됩니다.

슬롯 매핑 (CoreDropdownMenuStyle 7 필드)#

필드타입 (Flutter)적용 영역
popoverStyle CorePopoverStyle<Color, List<BoxShadow>>? 메뉴바에서 열린 submenu 팝업 패널 chrome
triggerStyle CoreButtonStyle<Color>? 메뉴바 자체 (위젯이 자체 트리거를 렌더링할 때)
itemStyle CoreButtonStyle<Color>? 각 menubar 항목 / submenu 항목 chrome
headerTextStyle CoreTextStyle<Color>? submenu 안 라벨 헤더
separatorColor Color? MenubarSeparator / MenubarDivider
separatorThicknessdouble?divider 두께
separatorIndentdouble?divider 좌우 indent

Migration — 옛 평면 필드 → 새 위치 매핑#

옛 chrome 필드새 위치
MenubarTheme.backgroundColor dropdownMenuStyle.popoverStyle.panelBackgroundColor (또는 Menubar.border + 기본 토큰)
MenubarTheme.borderColor dropdownMenuStyle.popoverStyle.panelBorderColor
MenubarTheme.borderRadius dropdownMenuStyle.popoverStyle.panelBorderRadius
MenubarTheme.padding dropdownMenuStyle.popoverStyle.panelPadding
MenubarTheme.subMenuOffset위젯 popoverOffset 으로 유지

사용 예 (Flutter)#

Menubar(
  dropdownMenuStyle: CoreDropdownMenuStyle(
    popoverStyle: CorePopoverStyle(
      panelBorderRadius: 8,
      panelBackgroundColor: Color(0xFFFFFFFF),
    ),
  ),
  children: [
    // MenuButton(...)
  ],
)

사용 예 (Web)#

Menubar(
  [
    MenubarItem(label: 'File', children: [
      MenubarAction(label: 'New', onSelect: () {}),
    ]),
  ],
  dropdownMenuStyle: CoreDropdownMenuStyle<String, String>(
    popoverStyle: CorePopoverStyle<String, String>(
      panelBorderRadius: 8,
    ),
  ),
)
속성타입기본값설명
labelString필수메뉴 바에 표시되는 레이블
menu List<MenubarEntry> 필수 클릭 시 표시할 드롭다운 메뉴 목록
속성타입기본값설명
valueString필수선택 시 onSelect에 전달되는 값
labelString필수메뉴 아이템 텍스트
shortcut String? null 키보드 단축키 표시 (예: ⌘S)
enabledbooltrue활성화 여부

메뉴 아이템 사이의 구분선입니다. 별도의 속성이 없습니다.

변형 (Variants)#

단축키 포함#

키보드 단축키를 함께 표시합니다.

Menubar(
  onSelect: handleMenubarSelect,
  items: [
    MenubarItem(
      label: '파일',
      menu: [
        MenubarMenu(value: 'new', label: '새 파일', shortcut: '⌘N'),
        MenubarMenu(value: 'open', label: '열기', shortcut: '⌘O'),
        MenubarMenu(value: 'save', label: '저장', shortcut: '⌘S'),
      ],
    ),
  ],
)

동작 스펙 (Behavior)#

인터랙션#

  • 클릭: 메뉴 바 항목 클릭 시 드롭다운 열기
  • 호버 이동: 한 메뉴가 열린 상태에서 다른 메뉴 바 항목 호버 시 자동 전환
  • 항목 선택: 드롭다운 항목 클릭 시 onSelect(value) 호출 후 닫힘
  • 외부 클릭: 메뉴 외부 클릭 시 닫힘

상태 전환#

  • closedopen: 메뉴 바 항목 클릭
  • openopen (다른 메뉴): 다른 메뉴 바 항목 호버
  • openclosed: 항목 선택, 외부 클릭, Escape

애니메이션#

  • 드롭다운 열림: 페이드 인 150ms
  • 드롭다운 닫힘: 페이드 아웃 100ms

사용 가이드라인 (Usage Guidelines)#

✅ Do#

메뉴 항목을 논리적 그룹으로 구분선으로 분리

MenubarItem(
  label: '파일',
  menu: [
    MenubarMenu(value: 'new', label: '새 파일', shortcut: '⌘N'),
    MenubarMenu(value: 'open', label: '열기', shortcut: '⌘O'),
    MenubarSeparator(), // 파일 작업과 저장 그룹 분리
    MenubarMenu(value: 'save', label: '저장', shortcut: '⌘S'),
    MenubarMenu(value: 'saveAs', label: '다른 이름으로 저장', shortcut: '⌘⇧S'),
    MenubarSeparator(), // 저장과 종료 분리
    MenubarMenu(value: 'exit', label: '종료', shortcut: '⌘Q'),
  ],
)

구분선으로 관련 항목을 그룹화하면 메뉴 탐색이 더 쉽고 직관적이다.


❌ Don't#

하나의 메뉴에 너무 많은 항목 사용 (10개 초과)

// ❌ 너무 많은 항목
MenubarItem(
  label: '도구',
  menu: List.generate(15, (i) => MenubarMenu(
    value: 'tool_$i',
    label: '도구 ${i + 1}',
  )),
)

항목이 너무 많으면 화면을 넘치거나 탐색이 어려워진다. 서브메뉴로 계층화하거나 항목을 줄인다.

✅ Do#

자주 사용하는 항목에 키보드 단축키 표시

MenubarMenu(value: 'save', label: '저장', shortcut: '⌘S')
MenubarMenu(value: 'undo', label: '실행 취소', shortcut: '⌘Z')
MenubarMenu(value: 'copy', label: '복사', shortcut: '⌘C')

단축키 표시가 파워 유저의 생산성을 높이고 메뉴를 통해 단축키를 학습하게 한다.


❌ Don't#

모바일 앱의 주요 네비게이션으로 Menubar 사용

// ❌ 모바일에 데스크탑 스타일 Menubar 강요
CouiMenubar(
  items: [
    MenubarItem(label: '홈', menu: []),
    MenubarItem(label: '검색', menu: []),
    MenubarItem(label: '프로필', menu: []),
  ],
  onSelect: handleMenubarSelect,
)

Menubar는 데스크탑 환경에 최적화되어 있다. 모바일에서는 NavigationBarDrawer가 적합하다.

✅ Do#

메뉴바 항목은 논리적 그룹으로 구성하세요.

CouiMenubar(
  items: [
    MenubarItem(label: '파일', children: fileMenuItems),
    MenubarItem(label: '편집', children: editMenuItems),
    MenubarItem(label: '보기', children: viewMenuItems),
    MenubarItem(label: '도움말', children: helpMenuItems),
  ],
)

메뉴바 항목을 기능 그룹으로 묶으면 사용자가 원하는 기능을 빠르게 찾을 수 있습니다.


❌ Don't#

메뉴바에 너무 많은 최상위 항목을 넣지 마세요.

// ❌ 15개의 최상위 메뉴바 항목
CouiMenubar(
  items: fifteenTopLevelItems,  // 너무 많아 화면을 넘침
)

최상위 메뉴바 항목은 7개 이하가 적절합니다. 너무 많으면 중요한 항목이 묻히거나 화면 밖으로 밀려납니다.

접근성 (Accessibility)#

키보드 인터랙션#

동작
Tab메뉴 바 항목 간 이동
Enter / Space포커스된 메뉴 바 항목 열기
Arrow Down메뉴 열기 + 첫 항목 포커스
Arrow Up/Down드롭다운 항목 간 이동
Arrow Left/Right메뉴 바 상위 항목 간 이동
Escape메뉴 닫기

스크린 리더#

  • Flutter: role="menubar" + role="menu" + role="menuitem" 자동 적용
  • Web: role="menubar" 메뉴바에, role="menu" 드롭다운에, role="menuitem" 항목에 자동 적용

터치 타겟#

  • 메뉴 바 항목 최소 높이: 44dp
  • 드롭다운 항목 최소 높이: 44dp

크로스 플랫폼 차이점 (Platform Differences)#

v3.0부터 기본 API (enabled, onChanged 등)가 통일되었습니다. 아래는 플랫폼 고유 차이점만 나열합니다.

항목FlutterWeb
클래스명CouiMenubarMenubar
위치수동 배치 (AppBar 아래 등)화면 상단 고정 또는 컨테이너 내
단축키 감지HardwareKeyboardKeyboardEvent
호버 전환MouseRegionCSS :hover
  • Menu: 사이드바 형태의 일반 메뉴
  • ContextMenu: 우클릭으로 표시되는 상황별 메뉴
  • Navigation: 페이지 이동을 위한 네비게이션 메뉴

조합 예제#

// 데스크탑 앱 레이아웃
Scaffold(
  body: Column(
    children: [
      // 메뉴 바
      CouiMenubar(
        onSelect: handleMenuSelect,
        items: [
          MenubarItem(
            label: '파일',
            menu: [
              MenubarMenu(value: 'new', label: '새 파일', shortcut: '⌘N'),
              MenubarMenu(value: 'open', label: '열기', shortcut: '⌘O'),
              const MenubarSeparator(),
              MenubarMenu(value: 'save', label: '저장', shortcut: '⌘S'),
            ],
          ),
          MenubarItem(
            label: '편집',
            menu: [
              MenubarMenu(value: 'undo', label: '실행 취소', shortcut: '⌘Z'),
              MenubarMenu(value: 'redo', label: '다시 실행', shortcut: '⌘⇧Z'),
            ],
          ),
        ],
      ),
      // 메인 콘텐츠 영역
      Expanded(child: mainContent),
    ],
  ),
)