Menubar
A persistent horizontal bar of menus — application-style File / Edit / View navigation, built from the dropdown menu's parts.
- Status
- Since
0.10.0- Accessibility pattern
- APG menubar
Last updated
Install
Configure registry credentials first using the Quickstart. Before copying this item or any transitive registry dependency, verify the signed manifest and save the exact bytes for Menubar before installation. Retain the digest printed by this command.
pnpm exec vegastack-design verify menubarInstall with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.
pnpm dlx shadcn@4.21.0 add @vegastack/menubarRun the exact offline --post-write command printed by the preflight, using its saved item path and independently retained --expected-integrity digest. Use the saved preflight for every transitive registry dependency too; stop on any mismatch before using the components.
The same command installs the registry items it composes: @vegastack/dropdown-menu, @vegastack/item.
Usage
import {
Menubar,
MenubarContent,
MenubarGroup,
MenubarItem,
MenubarMenu,
MenubarSeparator,
MenubarShortcut,
MenubarTrigger,
} from "@/components/ui/menubar";
<Menubar>
<MenubarMenu>
<MenubarTrigger>File</MenubarTrigger>
<MenubarContent>
<MenubarGroup>
<MenubarItem>
New Tab <MenubarShortcut>⌘T</MenubarShortcut>
</MenubarItem>
<MenubarItem>New Window</MenubarItem>
</MenubarGroup>
<MenubarSeparator />
<MenubarGroup>
<MenubarItem>Share</MenubarItem>
<MenubarItem>Print</MenubarItem>
</MenubarGroup>
</MenubarContent>
</MenubarMenu>
</Menubar>;Anatomy
Examples
Composition
Menubar is the bar; each MenubarMenu is one trigger plus its popup. Only the bar and the two
Menu-backed rows — MenubarCheckboxItem and MenubarRadioItem — are menubar's own; every other
part is a thin wrapper over the matching dropdown-menu part, which is why a fix there lands here
too.
Menubar
├── MenubarMenu
│ ├── MenubarTrigger
│ └── MenubarContent
│ ├── MenubarGroup
│ │ ├── MenubarLabel
│ │ ├── MenubarItem
│ │ └── MenubarItem
│ ├── MenubarSeparator
│ ├── MenubarGroup
│ │ ├── MenubarLabel
│ │ └── MenubarCheckboxItem
│ ├── MenubarSeparator
│ ├── MenubarGroup
│ │ ├── MenubarLabel
│ │ └── MenubarRadioGroup
│ │ ├── MenubarRadioItem
│ │ └── MenubarRadioItem
│ └── MenubarSub
│ ├── MenubarSubTrigger
│ └── MenubarSubContent
│ └── MenubarGroup
│ ├── MenubarLabel
│ └── MenubarItem
└── MenubarMenu
├── MenubarTrigger
└── MenubarContent
└── MenubarGroup
├── MenubarLabel
└── MenubarItemCheckbox
MenubarCheckboxItem renders role="menuitemcheckbox" with aria-checked and reserves the
inline-start indicator column. Rows that sit beside it take inset, so their labels line up with
the checked ones instead of with the indicator.
Radio
MenubarRadioGroup owns the value and each MenubarRadioItem is a role="menuitemradio";
selecting one clears the rest of its group.
Submenu
MenubarSub, MenubarSubTrigger and MenubarSubContent nest a second popup, which opens on
hover and on →. They wrap dropdown-menu's submenu parts, so the chevron, the
positioning and the theme scope are the same ones.
With Icons
A leading lucide-react icon is sized by the row itself, so pass a className rather than a
size prop. variant="destructive" tints the row, its icon and its focus wash.
RTL
The bar's own layout is flex with logical padding, and every row inherits dropdown-menu's
logical spacing, so a DirectionProvider set to rtl mirrors the bar, the popups and the submenu
chevron.
Description line
A row that needs a second line composes Item's parts: ItemContent › ItemTitle +
ItemDescription. MenubarItem, MenubarCheckboxItem and MenubarRadioItem each provide Item's description context, so the description's id
lands on the row's aria-describedby and a screen reader hears it as the row's description
(API-19). This is also where a disabled row says why: a disabled item keeps focus and shows its
reason as a description; Tooltip is not the reason carrier.
API Reference
MenubarContent adds no props of its own — it accepts everything the underlying element or Base UI primitive accepts (className, ref, ARIA attributes, event handlers).
| Prop | Type | Default | Description |
|---|---|---|---|
inset | boolean | — | |
variant | "default" | "destructive" | — |
| Prop | Type | Default | Description |
|---|---|---|---|
inset | boolean | — |
| Prop | Type | Default | Description |
|---|---|---|---|
inset | boolean | — |
| Prop | Type | Default | Description |
|---|---|---|---|
inset | boolean | — |
Menubar, MenubarMenu, MenubarTrigger, MenubarGroup, MenubarLabel, MenubarRadioGroup,
MenubarSeparator, MenubarShortcut, MenubarSub, MenubarSubContent and MenubarPortal add
no props of their own — each accepts everything the Base UI part, or the dropdown-menu part it
wraps, accepts.
Accessibility
- Renders Base UI's Menubar over Base UI's Menu: the bar is a
role="menubar"whose triggers move under ←/→, and each popup is arole="menu"ofmenuitem,menuitemcheckboxandmenuitemradiorows with type-ahead, ↑/↓ and Esc. - With one menu open, moving along the bar opens the next menu rather than merely focusing its trigger — the behaviour a desktop menubar has, and what the APG pattern specifies.
- Menubar opens no portal of its own:
MenubarContent,MenubarSubContentandMenubarPortalwrap thedropdown-menuparts that carry the theme scope across the portal (OVL-13), andmenubar.test.tsxasserts the rendered popup carries it, so the day that composition changes the test fails rather than the scope going quiet. - Rows carry the hand cursor from the one global rule in
base.css(INT-1). - A disabled item keeps focus and shows its reason as a description; Tooltip is not the reason carrier. ↑/↓ land on it and Enter does nothing — Base UI keeps every row focusable when disabled — and its pointer events stay alive too (FRM-4).
| Contract | States tested |
|---|---|
| Behaviour | default, open, closed, highlighted, checked, disabled, submenu-open |
| Accessibility | native-or-base-ui-semantics, browser-accessibility-test, keyboard-navigation, described, disabled |
| Visual | default, open, closed, highlighted, checked, disabled |
Do / Don't
Deviations
Upstream's file plus packages/ui/upstream/patches/menubar.patch. Every hunk:
- FOC-1 —
outline-hiddenis dropped fromMenubarTrigger. A trigger is a real focusable control (role="menuitem", in the bar's roving tab order), and upstream's class suppressed the one 2px:focus-visibleoutlinebase.cssowns, leaving it with no focus indicator at all. The two item parts keepoutline-hidden: they replace the outline withfocus:bg-accent, which is the sanctioned substitute affordance. - INT-1 —
cursor-defaultis removed fromMenubarCheckboxItemandMenubarRadioItem, the only two parts menubar renders fromMenuitself. Its item, submenu trigger, label, separator and shortcut composedropdown-menu's already-patched parts. - FRM-4 —
data-disabled:pointer-events-noneis dropped from those same two parts. - OVL-13 — no hunk, and that is the implementation, not an omission. Menubar opens no
portal of its own:
MenubarContent,MenubarSubContentandMenubarPortalare thin wrappers overDropdownMenuContent/DropdownMenuSubContent/DropdownMenuPortal, which carry the scope.menubar.test.tsxasserts the rendered popup carries it, so the day that composition changes the test fails rather than the scope going quiet. - BRD-1 —
MenubarContentandMenubarSubContentdraw a real 1pxborder border-borderinstead of upstream'sring-1 ring-foreground/10box-shadow outline. A border survives forced colours and matches Elevation; nothing else about the surface moves. - A11Y-13 — a focused destructive
MenubarItemreadsfocus:text-destructive-texton itsbg-destructive/10wash. Upstream inks it with the fill (focus:text-destructive), which measures 3.99:1 on its own tint in light, under the AA floor. The resting ink measures 4.77:1 in light and 6.21:1 in dark on the popup, over the floor, so it and the icon ink stay upstream's. - API-19 —
MenubarCheckboxItemandMenubarRadioItemeach provide Item'sItemDescriptionContextand pointaria-describedbyat anItemDescriptiononce one registers inside the row;MenubarIteminherits the same hunk fromDropdownMenuItem. A two-line row reads its second line as the row's accessible description (Description line). - DOC-2 —
import { cn } from "cn"becomes@vegastack/design; our npm layer ownscn. - DOC-1 — prettier reflow plus the
registry-stamp.mjsprovenance header; no decision of their own. - COL-24, INT-4 — hover and press come from the shared
state-layerutility, a translucent foreground layer (--state-hover,--state-pressed) over whatever fill is there, so the step reads the same on every surface in both themes; selected, open and highlighted fills arebg- state-selected/bg-state-hover. Operator, Regent, 2026-10-31.