Menubar | CoUI

Menubar

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

Menubar#

데스크탑 애플리케이션 스타일의 상단 가로 메뉴 바 컴포넌트입니다. 각 상위 메뉴는 클릭 시 드롭다운 패널을 열고, 키보드 단축키 힌트와 구분선을 지원합니다.

Menubar는 데이터 기반 컴포넌트입니다. CoreMenubarMenu 목록을 넘기면 각 메뉴가 가로 바에 라벨로 표시되고, entriesCoreMenubarEntry.action / CoreMenubarEntry.divider가 드롭다운 패널로 렌더링됩니다. Flutter와 Web이 동일한 파라미터 이름·동작을 갖습니다.

Live Preview#

Web
Flutter
Loading Flutter...
class MenubarDefaultExample extends StatelessComponent {
  const MenubarDefaultExample({super.key});

  @override
  Component build(BuildContext context) {
    return Menubar(
      menus: const [
        CoreMenubarMenu(
          label: 'File',
          entries: [
            CoreMenubarEntry.action(label: 'New', shortcut: 'Ctrl+N'),
            CoreMenubarEntry.action(label: 'Open', shortcut: 'Ctrl+O'),
            CoreMenubarEntry.divider(),
            CoreMenubarEntry.action(label: 'Exit'),
          ],
        ),
        CoreMenubarMenu(
          label: 'Edit',
          entries: [
            CoreMenubarEntry.action(label: 'Cut', shortcut: 'Ctrl+X'),
            CoreMenubarEntry.action(label: 'Copy', shortcut: 'Ctrl+C'),
            CoreMenubarEntry.action(label: 'Paste', shortcut: 'Ctrl+V'),
          ],
        ),
        CoreMenubarMenu(
          label: 'View',
          entries: [
            CoreMenubarEntry.action(label: 'Zoom In'),
            CoreMenubarEntry.action(label: 'Zoom Out'),
            CoreMenubarEntry.divider(),
            CoreMenubarEntry.action(label: 'Full Screen'),
          ],
        ),
      ],
    );
  }
}
class MenubarDefaultExample extends StatelessWidget {
  const MenubarDefaultExample({super.key});

  @override
  Widget build(BuildContext context) {
    return Menubar(
      menus: const [
        CoreMenubarMenu(
          label: 'File',
          entries: [
            CoreMenubarEntry.action(label: 'New', shortcut: 'Ctrl+N'),
            CoreMenubarEntry.action(label: 'Open', shortcut: 'Ctrl+O'),
            CoreMenubarEntry.divider(),
            CoreMenubarEntry.action(label: 'Exit'),
          ],
        ),
        CoreMenubarMenu(
          label: 'Edit',
          entries: [
            CoreMenubarEntry.action(label: 'Cut', shortcut: 'Ctrl+X'),
            CoreMenubarEntry.action(label: 'Copy', shortcut: 'Ctrl+C'),
            CoreMenubarEntry.action(label: 'Paste', shortcut: 'Ctrl+V'),
          ],
        ),
        CoreMenubarMenu(
          label: 'View',
          entries: [
            CoreMenubarEntry.action(label: 'Zoom In'),
            CoreMenubarEntry.action(label: 'Zoom Out'),
            CoreMenubarEntry.divider(),
            CoreMenubarEntry.action(label: 'Full Screen'),
          ],
        ),
      ],
    );
  }
}

사용 시기 (When to Use)#

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

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

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

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

기본 사용법 (Basic Usage)#

Menubar(
  menus: const [
    CoreMenubarMenu(
      label: 'File',
      entries: [
        CoreMenubarEntry.action(label: 'New', shortcut: 'Ctrl+N'),
        CoreMenubarEntry.action(label: 'Open', shortcut: 'Ctrl+O'),
        CoreMenubarEntry.divider(),
        CoreMenubarEntry.action(label: 'Exit'),
      ],
    ),
    CoreMenubarMenu(
      label: 'Edit',
      entries: [
        CoreMenubarEntry.action(label: 'Cut', shortcut: 'Ctrl+X'),
        CoreMenubarEntry.action(label: 'Copy', shortcut: 'Ctrl+C'),
        CoreMenubarEntry.action(label: 'Paste', shortcut: 'Ctrl+V'),
      ],
    ),
  ],
)
Menubar(
  menus: const [
    CoreMenubarMenu(
      label: 'File',
      entries: [
        CoreMenubarEntry.action(label: 'New', shortcut: 'Ctrl+N'),
        CoreMenubarEntry.action(label: 'Open', shortcut: 'Ctrl+O'),
        CoreMenubarEntry.divider(),
        CoreMenubarEntry.action(label: 'Exit'),
      ],
    ),
    CoreMenubarMenu(
      label: 'Edit',
      entries: [
        CoreMenubarEntry.action(label: 'Cut', shortcut: 'Ctrl+X'),
        CoreMenubarEntry.action(label: 'Copy', shortcut: 'Ctrl+C'),
        CoreMenubarEntry.action(label: 'Paste', shortcut: 'Ctrl+V'),
      ],
    ),
  ],
)

Props / Parameters#

속성타입기본값설명
menus List<CoreMenubarMenu> 필수 가로 바에 표시되는 상위 메뉴 목록
border bool true 가로 바에 외곽선 컨테이너를 그릴지 여부
variant CoreMenubarVariant standard 시각 변형
menubarStyle CoreMenubarStyle? null 인스턴스별 chrome / 치수 스타일

CoreMenubarMenu#

속성타입기본값설명
labelString필수가로 바에 표시되는 메뉴 라벨
entries List<CoreMenubarEntry> const [] 메뉴를 열면 표시되는 드롭다운 엔트리

CoreMenubarEntry#

생성자속성타입기본값설명
.action label String 필수 액션 행 텍스트
.action shortcut String? null 라벨 뒤에 표시되는 단축키 힌트
.action enabled bool true 비활성 시 muted 처리 + onSelect 미호출
.action onSelect CoreVoidCallback? null 활성 액션 선택 시 호출
.divider그룹 사이 구분선

스타일 시스템 (Style System)#

Menubar의 모든 chrome / 치수 오버라이드는 단일 CoreMenubarStyle 슬롯으로 흐릅니다 (Epic #1415 option D). 가로 바와 드롭다운 패널 chrome을 모두 한 클래스가 받습니다.

시맨틱 vs 스타일#

  • 시맨틱 / behaviour: 위젯 파라미터로 직접 (border, variant, menus)
  • chrome / dimensional: menubarStyle: CoreMenubarStyle? 한 곳으로

Resolve chain#

CoreMenubarStyle.defaultX                       // 디자인 시스템 기본값
  → CoreMenubarTheme.style                       // 프로젝트 공통
  → CoreMenubarTheme.variantStyles[variant]      // variant별 공통
  → widget.menubarStyle                          // 인스턴스별

CoreMenubarStyle 주요 필드#

필드타입적용 영역
barPaddingCoreEdgeInsets?가로 바 내부 패딩
barBorderRadiusCoreBorderRadius?가로 바 모서리 반경
barBorderWidthdouble?가로 바 외곽선 두께
barBackgroundColorCoreColor?가로 바 배경색
barBorderColorCoreColor?가로 바 외곽선 색
triggerButtonStyle CoreButtonStyle? 트리거 버튼 chrome (패딩 / 반경 — 트리거는 Button 합성, hover·open 색은 .menubar variant 소유)
entryButtonStyle CoreButtonStyle? 드롭다운 엔트리 버튼 chrome (엔트리도 Button 합성, hover·disabled 색은 .menu variant 소유)
menuPanelStyle CorePopupStyle? 드롭다운 패널 박스 chrome (패널은 Popup 합성 — 배경 / 반경 / 패딩 / 그림자 / 최소너비)
separatorStyle CoreSeparatorStyle? 구분선 엔트리 스타일 (색 / 두께 — Separator 합성)
shortcutLeftMargindouble?엔트리 단축키 힌트 좌측 여백
shortcutTextStyle CoreTextStyle? 엔트리 단축키 힌트 타이포그래피
separatorPaddingCoreEdgeInsets?구분선 래퍼 상하 패딩
labelTextStyleCoreTextStyle?트리거 라벨 타이포그래피
menuOffsetdouble?바와 드롭다운 패널 사이 간격
durationDuration?열림 / 닫힘 색상 전환 시간

사용 예 (Flutter / Web 동일)#

Menubar(
  menubarStyle: const CoreMenubarStyle(
    // 패널 박스는 Popup 합성 — chrome 은 menuPanelStyle 단일 슬롯으로.
    menuPanelStyle: CorePopupStyle(
      backgroundColor: CoreColor.token(CoreColors.surface),
      minWidth: CoreSpace.space160,
    ),
    // 트리거는 Button 합성 — padding / 반경은 triggerButtonStyle 로 흐른다.
    triggerButtonStyle: CoreButtonStyle(
      padding: CoreEdgeInsets.symmetric(
        horizontal: CoreSpace.space16,
        vertical: CoreSpace.space6,
      ),
    ),
  ),
  menus: const [
    CoreMenubarMenu(
      label: 'File',
      entries: [CoreMenubarEntry.action(label: 'New')],
    ),
  ],
)

변형 (Variants)#

단축키 포함#

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

Menubar(
  menus: const [
    CoreMenubarMenu(
      label: 'File',
      entries: [
        CoreMenubarEntry.action(label: 'New', shortcut: 'Ctrl+N'),
        CoreMenubarEntry.action(label: 'Open', shortcut: 'Ctrl+O'),
        CoreMenubarEntry.action(label: 'Save', shortcut: 'Ctrl+S'),
      ],
    ),
  ],
)

비활성 엔트리#

enabled: false 엔트리는 muted 처리되며 선택해도 onSelect가 호출되지 않습니다.

Menubar(
  menus: const [
    CoreMenubarMenu(
      label: 'File',
      entries: [
        CoreMenubarEntry.action(label: 'New'),
        CoreMenubarEntry.action(label: 'Exit', enabled: false),
      ],
    ),
  ],
)

동작 스펙 (Behavior)#

인터랙션#

  • 클릭: 메뉴 바 항목 클릭 시 드롭다운 열기. 같은 항목 재클릭 시 닫힘
  • 항목 선택: 활성 엔트리 클릭 시 onSelect 호출 후 닫힘
  • 외부 클릭: 메뉴 외부 클릭 시 닫힘
  • Escape: 열린 드롭다운 닫힘

상태 전환#

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

애니메이션#

  • 트리거 / 엔트리 hover 배경: 150ms 색상 전환 (CoreDuration.ms150)

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

✅ Do#

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

CoreMenubarMenu(
  label: 'File',
  entries: [
    CoreMenubarEntry.action(label: 'New', shortcut: 'Ctrl+N'),
    CoreMenubarEntry.action(label: 'Open', shortcut: 'Ctrl+O'),
    CoreMenubarEntry.divider(),
    CoreMenubarEntry.action(label: 'Save', shortcut: 'Ctrl+S'),
    CoreMenubarEntry.divider(),
    CoreMenubarEntry.action(label: 'Exit'),
  ],
)

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


❌ Don't#

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

// ❌ 너무 많은 항목
CoreMenubarMenu(
  label: 'Tools',
  entries: List.generate(
    15,
    (i) => CoreMenubarEntry.action(label: 'Tool ${i + 1}'),
  ),
)

항목이 너무 많으면 화면을 넘치거나 탐색이 어려워진다. 항목을 줄이거나 메뉴를 나눈다.

✅ Do#

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

CoreMenubarEntry.action(label: 'Save', shortcut: 'Ctrl+S')
CoreMenubarEntry.action(label: 'Undo', shortcut: 'Ctrl+Z')
CoreMenubarEntry.action(label: 'Copy', shortcut: 'Ctrl+C')

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


❌ Don't#

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

// ❌ 모바일에 데스크탑 스타일 Menubar 강요
Menubar(
  menus: const [
    CoreMenubarMenu(label: 'Home'),
    CoreMenubarMenu(label: 'Search'),
    CoreMenubarMenu(label: 'Profile'),
  ],
)

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

접근성 (Accessibility)#

시맨틱 역할#

  • 가로 바: role="menubar" + aria-orientation="horizontal"
  • 상위 메뉴 트리거: role="menuitem" + aria-haspopup + aria-expanded
  • 드롭다운 패널: role="menu"
  • 드롭다운 엔트리: role="menuitem" (비활성 시 aria-disabled)
  • 구분선: role="separator"

키보드 인터랙션#

동작
Escape열린 드롭다운 닫기

스크린 리더#

  • Flutter: Semantics로 menubar / button / expanded 상태 전달
  • Web: role="menubar" / role="menu" / role="menuitem" / role="separator" 자동 적용

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

Menubar는 Flutter와 Web이 동일한 menus / border / variant / menubarStyle API와 동작을 갖습니다. 드롭다운 패널은 Flutter에서는 OverlayPortal, Web에서는 루트 OverlayHost의 menu 레이어로 portal되어 양쪽 모두 ancestor overflow에 잘리지 않습니다.

  • Menu: 사이드바 형태의 일반 메뉴
  • ContextMenu: 우클릭으로 표시되는 상황별 메뉴
  • Navigation: 페이지 이동을 위한 네비게이션 메뉴