Skip to content
Component installs need the registry setup
VegaStack Design

Menubar

A persistent horizontal bar of menus — application-style File / Edit / View navigation, built from the dropdown menu's parts.

Status
stable
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 menubar

Install with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.

pnpm dlx shadcn@4.21.0 add @vegastack/menubar

Run 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

Menubar — data-slot="menubar"
MenubarPortal — data-slot="menubar-portal"
MenubarMenu — data-slot="menubar-menu"
MenubarTrigger — data-slot="menubar-trigger"
MenubarContent — data-slot="menubar-content"
MenubarGroup — data-slot="menubar-group"
MenubarSeparator — data-slot="menubar-separator"
MenubarLabel — data-slot="menubar-label"
MenubarItem — data-slot="menubar-item"
MenubarShortcut — data-slot="menubar-shortcut"
MenubarCheckboxItem — data-slot="menubar-checkbox-item"
MenubarRadioGroup — data-slot="menubar-radio-group"
MenubarRadioItem — data-slot="menubar-radio-item"
MenubarSub — data-slot="menubar-sub"
MenubarSubTrigger — data-slot="menubar-sub-trigger"
MenubarSubContent — data-slot="menubar-sub-content"

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
            └── MenubarItem

Checkbox

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.

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).

PropTypeDefaultDescription
insetboolean—
variant"default" | "destructive"—
PropTypeDefaultDescription
insetboolean—
PropTypeDefaultDescription
insetboolean—
PropTypeDefaultDescription
insetboolean—

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 a role="menu" of menuitem, menuitemcheckbox and menuitemradio rows 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, MenubarSubContent and MenubarPortal wrap the dropdown-menu parts that carry the theme scope across the portal (OVL-13), and menubar.test.tsx asserts 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).
ContractStates tested
Behaviourdefault, open, closed, highlighted, checked, disabled, submenu-open
Accessibilitynative-or-base-ui-semantics, browser-accessibility-test, keyboard-navigation, described, disabled
Visualdefault, open, closed, highlighted, checked, disabled

Do / Don't

Do
Use a menubar for an application-shaped surface with a stable, well-known command set — an editor, a canvas, a console.
Don't
Use it as site navigation; a persistent command bar is not a nav, and a Navigation Menu or a Sidebar is.

Deviations

Upstream's file plus packages/ui/upstream/patches/menubar.patch. Every hunk:

  • FOC-1 — outline-hidden is dropped from MenubarTrigger. 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-visible outline base.css owns, leaving it with no focus indicator at all. The two item parts keep outline-hidden: they replace the outline with focus:bg-accent, which is the sanctioned substitute affordance.
  • INT-1 — cursor-default is removed from MenubarCheckboxItem and MenubarRadioItem, the only two parts menubar renders from Menu itself. Its item, submenu trigger, label, separator and shortcut compose dropdown-menu's already-patched parts.
  • FRM-4 — data-disabled:pointer-events-none is 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, MenubarSubContent and MenubarPortal are thin wrappers over DropdownMenuContent / DropdownMenuSubContent / DropdownMenuPortal, which carry the scope. menubar.test.tsx asserts the rendered popup carries it, so the day that composition changes the test fails rather than the scope going quiet.
  • BRD-1 — MenubarContent and MenubarSubContent draw a real 1px border border-border instead of upstream's ring-1 ring-foreground/10 box-shadow outline. A border survives forced colours and matches Elevation; nothing else about the surface moves.
  • A11Y-13 — a focused destructive MenubarItem reads focus:text-destructive-text on its bg-destructive/10 wash. 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 — MenubarCheckboxItem and MenubarRadioItem each provide Item's ItemDescriptionContext and point aria-describedby at an ItemDescription once one registers inside the row; MenubarItem inherits the same hunk from DropdownMenuItem. 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 owns cn.
  • DOC-1 — prettier reflow plus the registry-stamp.mjs provenance header; no decision of their own.
  • COL-24, INT-4 — hover and press come from the shared state-layer utility, 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 are bg- state-selected / bg-state-hover. Operator, Regent, 2026-10-31.

On this page