Menubar | CoUI
본문으로 건너뛰기
LogoCoUI

Menubar

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

목차

데스크탑 애플리케이션 스타일의 상단 가로 메뉴 바 컴포넌트입니다. 각 상위 메뉴는 클릭 시 드롭다운 패널을 열고, 한 번 열린 뒤에는 다른 메뉴에 마우스를 올리는 것만으로 전환되며, 키보드 단축키 힌트와 구분선을 지원합니다.

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

기본 사용법 (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'),
      ],
    ),
  ],
)

Live Preview#

사용 시기 (When to Use)#

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

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

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

  • NavigationMenu: 앱 액션 메뉴 대신 상위 사이트 링크와 드롭다운 콘텐츠를 구성할 때
  • DropdownMenu: 여러 상위 메뉴 대신 단일 버튼의 액션 목록이 필요할 때
  • Tabs: 오버레이 메뉴 대신 같은 화면의 패널 하나를 교체할 때

이런 곳에는 쓰지 마세요:

  • 트리거 바의 intrinsic 폭보다 좁은 컨테이너 — 좁으면 플랫폼마다 다르게 깨집니다 (아래 "크로스 플랫폼 차이점" 참조).

Props / Parameters#

속성타입기본값설명
menus List<CoreMenubarMenu> 필수 가로 바에 표시되는 상위 메뉴 목록
border bool true 가로 바에 외곽선 컨테이너를 그릴지 여부
activeIndex int? null 열린 메뉴 인덱스 (controlled 모드). null → 내부 상태로 추적
onActiveIndexChanged void Function(int?)? null 열린 메뉴가 바뀔 때 호출 (닫힘은 null)
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 슬롯으로 흐릅니다. 가로 바 chrome, 트리거 버튼 chrome, 드롭다운 패널 박스 chrome을 한 클래스가 받습니다. 드롭다운 안의 행 chrome(엔트리 버튼 · 구분선 · 단축키 힌트 타이포그래피)은 합성된 Menu가 소유하므로 CoreMenuStyle / CoreMenuTheme로 조정합니다.

시맨틱 vs 스타일#

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

Resolve chain#

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

CoreMenubarStyle 필드#

필드타입설명
surfaceStyle CoreSurfaceStyleId? Material override for this surface; null inherits the surrounding material. Deliberately no default: null inherits the active theme material axis; a fixed default would override that axis before the resolver sees it.
barPadding CoreEdgeInsets? Internal padding of the horizontal bar container.
barBorderRadius CoreBorderRadius? Corner radius of the horizontal bar container.
barBorderWidth double? Border width of the horizontal bar container (logical px).
barBackgroundColor CoreColor? Background colour of the horizontal bar.
barBorderColor CoreColor? Border colour of the horizontal bar.
triggerButtonStyle CoreButtonStyle? Nested style for the top-level trigger Button(variant: menubar) — the single source for the trigger's button chrome (padding + highlight radius). Resolvers raw-forward defaultTriggerButtonStyle.merge(this) to the composed Button (no resolver-side assembly). null falls back to [defaultTriggerButtonStyle].
menuPanelStyle CorePopupStyle? Nested panel-box chrome for the dropdown menu panel — the dropdown is a Popup (composed by the panel Menu ), so the resolver raw-forwards this (merged over [defaultMenuPanelStyle]) into the composed Menu 's panel slot. Overlaid by the per-instance value.
menuOffset double? Vertical gap between the bar and a dropdown panel (logical px).
labelTextStyle CoreTextStyle? Per-instance override for the top-level trigger label typography — overlaid on the labelLarge role / [defaultLabelTextStyle]. Text colour is carried via [CoreTextStyle.color] inside this slot.

CoreMenubarStyle 변형별 기본값 (CoreMenubarVariantStyle)#

필드standard
barBackgroundColorsurface
barBorderColoroutline (fallback: outlineVariant)

사용 예 (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')],
    ),
  ],
)

빠른 오버라이드 (Chain)#

이미 만든 Menubar 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다.

class MenubarChainExample extends StatelessWidget {
  const MenubarChainExample({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'),
          ],
        ),
      ],
    ).withStyle(
      const CoreMenubarStyle(
        barPadding: CoreEdgeInsets.all(CoreSpace.space8),
        barBorderRadius: CoreBorderRadius.all(CoreRadius.radius16),
        barBorderWidth: CoreStrokeWidth.stroke2,
        barBackgroundColor: CoreColor.token(CoreColors.surfaceContainerHigh),
        barBorderColor: CoreColor.token(CoreColors.primary),
      ),
    );
  }
}
class MenubarChainExample extends StatelessComponent {
  const MenubarChainExample({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'),
          ],
        ),
      ],
    ).withStyle(
      const CoreMenubarStyle(
        barPadding: CoreEdgeInsets.all(CoreSpace.space8),
        barBorderRadius: CoreBorderRadius.all(CoreRadius.radius16),
        barBorderWidth: CoreStrokeWidth.stroke2,
        barBackgroundColor: CoreColor.token(CoreColors.surfaceContainerHigh),
        barBorderColor: CoreColor.token(CoreColors.primary),
      ),
    );
  }
}

변형 (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)#

인터랙션#

  • 클릭: 메뉴 바 항목 클릭 시 드롭다운 열기. 같은 항목 재클릭 시 닫힘
  • 호버 전환: 메뉴가 이미 열려 있을 때 다른 메뉴 바 항목에 마우스를 올리면 클릭 없이 그 메뉴로 전환. 데스크톱 메뉴바(macOS / Windows)와 동일하게, 첫 클릭이 바를 "무장"시키고 그 뒤로는 포인터를 따라간다
  • 항목 선택: 합성된 Menu의 onActivate로 먼저 닫기를 요청하고 활성 엔트리의 onSelect 호출
  • 외부 클릭: 메뉴 외부 클릭 시 닫힘
  • Escape: Web의 열린 드롭다운 닫힘. Flutter의 현재 non-modal 메뉴 경로는 Escape 닫힘을 제공하지 않음
  • 긴 목록: 드롭다운 항목이 뷰포트가 패널에 남기는 높이보다 길면, 패널이 남는 높이를 모두 차지하고 위쪽 여백에 붙어 열리며 행이 패널 안에서 스크롤됩니다
  • 열린 채로 menus 변경: 열려 있는 동안 menus 가 바뀌면 열린 드롭다운이 새 항목으로 갱신됩니다

닫힌 상태에서의 호버는 아무 일도 하지 않는다. 바 위를 지나가기만 해도 메뉴가 열리면 포인터가 바를 가로지를 때마다 메뉴가 튀어나오기 때문이다 (호버만으로 여는 NavigationMenu 와 의도적으로 다른 지점). 호버가 메뉴를 닫지도 않는다 — 포인터는 아래 패널에 도달하려면 반드시 트리거를 벗어나므로, 닫힘은 항목 선택 / 외부 클릭 / Escape 가 담당한다.

호버 전환은 마우스 포인터에서만 동작한다. Flutter 는 MouseRegion 기반이라 구조적으로 터치를 보고하지 않고, Web 은 (hover: hover) 미디어 쿼리로 게이트한다 — 터치 브라우저가 탭에서 mouseenter 를 에뮬레이트해 "전환 후 즉시 닫힘" 이 되는 것을 막는다. 같은 capability, 각 플랫폼의 idiom.

상태 전환#

  • activeIndex가 non-null이면 호출부 값이 열린 메뉴를 정하고 변경은 onActiveIndexChanged로 요청합니다. null이면 내부 인덱스를 사용합니다. 메뉴 인덱스는 0-based이며 그룹 비활성 플래그는 없습니다.
  • closed → open: 메뉴 바 항목 클릭 (호버는 열지 않음)
  • open → open (다른 메뉴): 다른 메뉴 바 항목 클릭 또는 호버
  • open → closed: 항목 선택, 외부 클릭, Escape

애니메이션#

  • 트리거 hover / open 배경: 합성된 Button(variant: .menubar)의 색상 전환 (CoreButtonStyle.defaultAnimationDuration = CoreDuration.fast, 150ms) — menubarStyle.triggerButtonStyle.animationDuration로 조정
  • 드롭다운 패널 열림 / 닫힘: 패널 Popup의 모션 (menubarStyle.menuPanelStyle.openAnimationDuration / closeAnimationDuration)

사용 가이드라인 (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는 데스크탑 환경에 최적화되어 있다. 모바일에서는 NavigationBar나 Drawer가 적합하다.

트리거 바의 intrinsic 폭보다 좁은 컨테이너에 두기

// ❌ 메뉴 라벨을 다 담지 못하는 폭
SizedBox(
  width: 120,
  child: Menubar(
    menus: const [
      CoreMenubarMenu(label: 'File'),
      CoreMenubarMenu(label: 'Edit'),
      CoreMenubarMenu(label: 'View'),
    ],
  ),
)

Menubar 는 트리거를 줄이거나 감싸거나 스크롤하지 않는다. 좁아지면 Flutter 는 오버플로 경고를 그리고, Web 은 아무 신호 없이 테두리 밖으로 흘러넘친다. 이 갈림은 의도적으로 해소하지 않은 상태이므로, 모든 트리거가 들어가는 폭을 호출자가 보장한다.

접근성 (Accessibility)#

역할#

요소FlutterWeb
가로 바 Semantics(container: true, label: ...) — 로컬라이제이션 menubarLabel("메뉴 막대") role="menubar" + aria-orientation="horizontal" + 같은 값의 aria-label
상위 메뉴 트리거 Semantics(expanded:) 로 감싼 Button (버튼 시맨틱은 Clickable 이 제공) <button role="menuitem" aria-haspopup="true" aria-expanded=…> (열려 있으면 aria-selected="true" )
드롭다운 패널 합성된 Menu의 menu 역할. 별도 패널 라벨은 없음 role=menu · aria-orientation=vertical (라벨 없음)
드롭다운 엔트리 Menu 가 각 행의 시맨틱을 제공 Menu 가 menuitem 역할·비활성 상태·roving tabindex 를 제공
구분선 Semantics(label: ...) — dividerSeparatorLabel ("구분선"). Flutter 에 separator role 이 없어 이름으로 대신합니다 role="separator"

Web 루트의 role / aria-orientation / aria-label 은 호출자 attributes 를 펼친 뒤에 기록되므로 호출자가 덮을 수 없습니다.

키보드#

키FlutterWeb
Tab 트리거마다 개별 tab stop 바 전체가 tab stop 하나(마지막으로 머문 트리거)
ArrowLeft / ArrowRight 프레임워크 방향성 포커스 이동 바에서: 이전/다음 트리거(끝에서 순환), 열린 메뉴는 포커스된 트리거를 따라감 · 패널 안에서: 옆 메뉴를 열고 첫 항목으로
Home / End없음첫 / 마지막 트리거
ArrowDown / ArrowUp 없음 트리거에서 메뉴를 열고 첫 / 마지막 항목으로 포커스 · 패널 안에서는 항목 사이 이동(Menu)
Enter / Space메뉴 열기/닫기메뉴 열기/닫기
Escape비모달 경로에는 배선 없음열린 드롭다운 닫고 트리거로 포커스 복귀
Tab (패널 안)—드롭다운을 닫고 트리거로 포커스 복귀

Web 은 WAI-ARIA menubar 패턴을 따릅니다. 패널은 문서 끝의 오버레이 레이어로 portal 되므로, 패널 안의 Tab 은 네이티브 순서로 흘려보내지 않고 바로 되돌립니다.

브라우저 검증 — 이 컴포넌트의 Web 키보드 이동은 야간 파리티 스윕의 키보드 프로브(tool/parity/keyboard_probe.dart)가 하이드레이션된 문서 사이트에서 실제로 키를 눌러, 포커스가 기대한 항목으로 옮겨 갔는지 확인합니다. 핸들러가 달렸는지가 아니라 포커스가 어디에 있는지를 봅니다.

스크린 리더#

접근 가능한 이름의 출처:

  • 바 — 로컬라이제이션 menubarLabel (양 플랫폼 동일 문자열).
  • 트리거 · 드롭다운 행 — CoreMenubarMenu.label / CoreMenubarEntry.label 텍스트.
  • 구분선 — Web 은 role="separator" 로, Flutter 는 dividerSeparatorLabel 이름으로 읽힙니다.

메뉴가 열리고 닫히는 것을 알리는 live region 은 없습니다. 상태는 트리거의 expanded / aria-expanded 로만 드러납니다.

Flutter 드롭다운 패널은 menu 역할과 menuItem 행 의미를 제공합니다. 패널 자체의 이름은 따로 제공하지 않습니다. 양쪽 드롭다운은 비모달이라 모달 패널에 붙는 닫기 접근성 동작(Flutter dismiss, Web 의 숨긴 닫기 노드)도 없습니다.

포커스 관리#

  • 진입 — Flutter 는 modal: false 라 전용 패널 포커스를 요청하지 않습니다. Web 은 트리거에서 ArrowDown / ArrowUp 으로 열면 첫/마지막 행으로 이동합니다.
  • 이탈 — Web 은 Escape 와 패널 안의 Tab 에서 트리거로 복귀합니다. Flutter 는 명시적 복귀 경로가 없습니다.
  • 트랩 — 없음. Web 의 패널 Tab 은 포털의 문서 순서로 흘려보내는 대신 패널을 닫고 트리거로 돌립니다.
  • 포커스 표시 — Flutter 트리거는 disableFocusOutline: true 라 포커스 링을 그리지 않습니다. .menubar variant 의 focusedBackground 만 남는데 useFocusedAsHover: true 로 hover 와 같은 색이라 키보드 포커스와 마우스 호버를 구분할 수 없습니다. Web 트리거는 Button 기본값이라 focus-visible:outline-* 링이 그대로 남습니다.

알려진 제약#

  • Flutter 는 roving 탐색이 아닙니다. 트리거와 행마다 tab stop 이 생기며, Web 의 메뉴 의미론을 갖춘 순환 탐색과 다릅니다.
  • Flutter 에서는 Escape 로 닫을 수 없습니다. 대안: 패널 바깥을 탭하거나, activeIndex / onActiveIndexChanged 로 제어 모드를 써서 호출부가 직접 닫으세요.
  • 트리거와 패널이 접근성 트리에서 이어지지 않습니다. Web 패널은 오버레이 레이어로 포탈되는데 aria-controls 가 없어 role="menu" 가 role="menubar" 의 자식으로 놓이지 않습니다. 대안 없음 — 패널의 attributes 진입점이 노출되지 않습니다.

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

menus / border / variant / menubarStyle을 공유하지만 키보드 탐색은 아래처럼 다릅니다. 드롭다운 패널은 Flutter에서는 PopoverOverlayHandler, Web에서는 루트 OverlayHost의 menu 레이어로 portal되어 양쪽 모두 ancestor overflow에 잘리지 않습니다.

항목FlutterWeb
트리거 탐색버튼별 TabLeft / Right · Home / End의 roving
패널 진입버튼 활성화 후 오버레이Down 첫 행 / Up 마지막 행
패널 탐색항목별 포커스Menu의 roving · typeahead; Left / Right로 이웃 메뉴 전환
패널 Tab일반 포커스 이동패널 닫기 요청 후 트리거 복귀
긴 메뉴 엔진이 패널 최대 높이를 레이아웃 제약으로 제한 (여백·시스템 바·키보드 제외) 위치 엔진이 패널을 측정한 뒤 상자에 max-height 를 걸고 상자 안의 목록을 overflow-y: auto 로 스크롤 (뷰포트 여백 viewportPadding 만 제외)

좁은 컨테이너 (의도된 미해소) — 컨테이너가 트리거 바의 intrinsic 폭보다 좁으면 두 플랫폼이 다르게 깨집니다. Flutter 는 RenderFlex overflowed 경고와 오버플로 스트라이프를 그리고, Web 은 아무 신호 없이 테두리 밖으로 흘러넘칩니다. 가로 스크롤·clip 으로 맞추지 않고 이 상태를 유지하기로 했으므로, 트리거가 모두 들어가는 폭에서만 사용하세요.

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