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
Add Menubar from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.
pnpm dlx shadcn@latest add @vegastack/menubarThe same command installs the registry items it composes: @vegastack/dropdown-menu.
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.
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 row keeps its pointer events (FRM-4), so a Tooltip can explain why it is unavailable.
| Contract | States tested |
|---|---|
| Behaviour | default, open, closed, highlighted, checked, disabled, submenu-open |
| Accessibility | native-or-base-ui-semantics, browser-accessibility-test, keyboard-navigation |
| 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. - 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.