Menubar#
데스크탑 애플리케이션 스타일의 상단 가로 메뉴 바 컴포넌트입니다. 각 상위 메뉴는 클릭 시 드롭다운 패널을 열고, 한 번 열린 뒤에는 다른 메뉴에 마우스를 올리는 것만으로 전환되며, 키보드 단축키 힌트와 구분선을 지원합니다.
Menubar는 데이터 기반 컴포넌트입니다. CoreMenubarMenu 목록을 넘기면 각
메뉴가 가로 바에 라벨로 표시되고, entries의 CoreMenubarEntry.action
/
CoreMenubarEntry.divider가 드롭다운 패널로 렌더링됩니다. Flutter와 Web이
동일한 파라미터 이름·동작을 갖습니다.
Live Preview#
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'),
],
),
],
);
}
}
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),
),
);
}
}
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),
),
);
}
}
사용 시기 (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#
Menubar#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
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#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
label | String | 필수 | 가로 바에 표시되는 메뉴 라벨 |
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 필드#
| 필드 | 타입 | 설명 |
|---|---|---|
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 |
|---|---|
barBackgroundColor | surface |
barBorderColor | outline (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)와 동일하게, 첫 클릭이 바를 "무장"시키고 그 뒤로는 포인터를 따라간다
- 항목 선택: 활성 엔트리 클릭 시
onSelect호출 후 닫힘 - 외부 클릭: 메뉴 외부 클릭 시 닫힘
- Escape: 열린 드롭다운 닫힘
닫힌 상태에서의 호버는 아무 일도 하지 않는다. 바 위를 지나가기만 해도 메뉴가 열리면 포인터가 바를 가로지를 때마다 메뉴가 튀어나오기 때문이다 (호버만으로 여는
NavigationMenu 와 의도적으로 다른 지점). 호버가 메뉴를 닫지도 않는다 — 포인터는 아래 패널에 도달하려면 반드시 트리거를 벗어나므로, 닫힘은 항목 선택 / 외부 클릭 / Escape 가 담당한다.
호버 전환은 마우스 포인터에서만 동작한다. Flutter 는 MouseRegion 기반이라 구조적으로 터치를 보고하지 않고, Web 은
(hover: hover) 미디어 쿼리로 게이트한다 — 터치 브라우저가 탭에서 mouseenter 를 에뮬레이트해 "전환 후 즉시 닫힘" 이 되는 것을 막는다. 같은 capability, 각 플랫폼의 idiom.
상태 전환#
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가 적합하다.
접근성 (Accessibility)#
시맨틱 역할#
-
가로 바:
role="menubar"+aria-orientation="horizontal"+ 로컬라이제이션menubarLabel에서 온aria-label(Flutter 는 같은 값을Semantics(label: ...)로) - 상위 메뉴 트리거:
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에 잘리지 않습니다.
관련 컴포넌트 (Related Components)#
- Menu: 사이드바 형태의 일반 메뉴
- ContextMenu: 우클릭으로 표시되는 상황별 메뉴
- Navigation: 페이지 이동을 위한 네비게이션 메뉴