Skip to content
Component installs need the registry setup— the Base UI shadcn project, the @vegastack namespace and the Cloudflare Access service token.
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

Add Menubar from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.

pnpm dlx shadcn@latest add @vegastack/menubar

The 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

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.

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 row keeps its pointer events (FRM-4), so a Tooltip can explain why it is unavailable.
ContractStates tested
Behaviourdefault, open, closed, highlighted, checked, disabled, submenu-open
Accessibilitynative-or-base-ui-semantics, browser-accessibility-test, keyboard-navigation
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-1outline-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-1cursor-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-4data-disabled:pointer-events-none is dropped from those same two parts.
  • OVL-13no 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.
  • DOC-2import { 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.

On this page