Skip to main content
Gremorie

Menubar

Desktop-app-style horizontal multi-menu bar. File, Edit, View, Help with submenus, checkboxes, radios, and shortcuts.

Overview

Menubar is the desktop-app menu bar primitive: several menus laid out side by side (File, Edit, View, Help), each opening a dropdown of items, checkboxes, radios, and nested submenus. Built on Radix Menubar, it implements the cross-menu keyboard contract users expect from native menus - once one menu is open, arrow keys walk into the next without re-pressing the trigger.

Reach for Menubar when you are emulating a desktop application - code editors, spreadsheets, image editors, IDEs. For everything else - site nav, single-button action menus, settings panels - use NavigationMenu or DropdownMenu instead.

Preview

'use client';import {  Menubar,  MenubarCheckboxItem,  MenubarContent,  MenubarItem,  MenubarMenu,  MenubarRadioGroup,  MenubarRadioItem,  MenubarSeparator,  MenubarShortcut,  MenubarTrigger,} from '@gremorie/rx-navigation';export function MenubarPreview() {  return (    <Menubar>      <MenubarMenu>        <MenubarTrigger>File</MenubarTrigger>        <MenubarContent>          <MenubarItem>            New tab <MenubarShortcut>CtrlT</MenubarShortcut>          </MenubarItem>          <MenubarItem>            New window <MenubarShortcut>CtrlN</MenubarShortcut>          </MenubarItem>          <MenubarSeparator />          <MenubarItem>Print...</MenubarItem>        </MenubarContent>      </MenubarMenu>      <MenubarMenu>        <MenubarTrigger>View</MenubarTrigger>        <MenubarContent>          <MenubarCheckboxItem checked>            Always show bookmarks          </MenubarCheckboxItem>          <MenubarCheckboxItem>Show full URL</MenubarCheckboxItem>          <MenubarSeparator />          <MenubarRadioGroup value="comfortable">            <MenubarRadioItem value="compact">Compact</MenubarRadioItem>            <MenubarRadioItem value="comfortable">Comfortable</MenubarRadioItem>          </MenubarRadioGroup>        </MenubarContent>      </MenubarMenu>    </Menubar>  );}

Anatomy

Menubar
└─ MenubarMenu                      one top-level menu (File, Edit, …)
   ├─ MenubarTrigger                clickable label that opens the menu
   └─ MenubarContent                portaled dropdown panel
      ├─ MenubarItem                command row (inset / destructive variants)
      │  └─ MenubarShortcut         right-aligned keyboard hint
      ├─ MenubarCheckboxItem        toggleable item with a check indicator
      ├─ MenubarRadioGroup          single-choice group
      │  └─ MenubarRadioItem        one radio option with a dot indicator
      ├─ MenubarLabel               non-interactive group header
      ├─ MenubarSeparator           1px divider between groups
      └─ MenubarSub                 nested submenu
         ├─ MenubarSubTrigger       row that opens the submenu
         └─ MenubarSubContent       the submenu panel

Installation

bash npx gremorie@latest add rx-menubar
bash pnpm dlx gremorie@latest add rx-menubar
bash yarn dlx gremorie@latest add rx-menubar
bash bunx --bun gremorie@latest add rx-menubar

Usage

import {
  Menubar,
  MenubarContent,
  MenubarItem,
  MenubarMenu,
  MenubarSeparator,
  MenubarShortcut,
  MenubarTrigger,
} from "@gremorie/rx-navigation";

export function AppMenubar() {
  return (
    <Menubar>
      <MenubarMenu>
        <MenubarTrigger>File</MenubarTrigger>
        <MenubarContent>
          <MenubarItem>
            New file <MenubarShortcut>CtrlN</MenubarShortcut>
          </MenubarItem>
          <MenubarItem>Open...</MenubarItem>
          <MenubarSeparator />
          <MenubarItem>
            Save <MenubarShortcut>CtrlS</MenubarShortcut>
          </MenubarItem>
        </MenubarContent>
      </MenubarMenu>
      <MenubarMenu>
        <MenubarTrigger>Edit</MenubarTrigger>
        <MenubarContent>
          <MenubarItem>Undo</MenubarItem>
          <MenubarItem>Redo</MenubarItem>
        </MenubarContent>
      </MenubarMenu>
    </Menubar>
  );
}

The Angular edition of this component ships from source today (see the workbench for the side-by-side); its registry entry is coming next.

API

Root container. Wraps Radix Menubar.Root. Renders a horizontal flex h-9 items-center gap-1 rounded-md border bg-background p-1 shadow-xs bar.

PropTypeDefaultDescription
defaultValuestring-Uncontrolled initially open menu.
valuestring-Controlled open menu.
onValueChange(value: string) => void-Fires when the open menu changes.
dir"ltr" | "rtl""ltr"Reading direction.
loopbooleantrueWhether keyboard focus loops at the ends of the bar.

A single menu (trigger plus content). Wraps Radix Menubar.Menu.

PropTypeDefaultDescription
valuestring-Identifier used by Root's controlled value.

The bar button that opens the menu. Wraps Radix Menubar.Trigger.

PropTypeDefaultDescription
disabledbooleanfalseDisables this trigger.

The dropdown panel. Wraps Radix Menubar.Content inside a MenubarPortal.

PropTypeDefaultDescription
align"start" | "center" | "end""start"Horizontal alignment relative to the trigger.
alignOffsetnumber-4Pixel offset from the alignment edge.
sideOffsetnumber8Distance in pixels between the trigger and the content.
side"top" | "right" | "bottom" | "left""bottom"Preferred side.
loopbooleantrueWhether keyboard focus loops at the ends of the panel.

A standard action item.

PropTypeDefaultDescription
insetbooleanfalseWhen true, adds left padding so the item aligns with checkbox / radio items.
variant"default" | "destructive""default""destructive" paints the item and its icon in the destructive token.
disabledbooleanfalseDisables this item.
onSelect(event: Event) => void-Fires on activation. Call event.preventDefault() to keep the menu open.

A toggle item rendered with a leading check indicator.

PropTypeDefaultDescription
checkedboolean | "indeterminate"falseCurrent state.
onCheckedChange(checked: boolean) => void-Fires when the state changes.
disabledbooleanfalseDisables this item.

A radio set rendered with a leading dot indicator.

ComponentPropTypeDescription
RadioGroupvaluestringControlled selected value.
RadioGrouponValueChange(value: string) => voidFires when the selected value changes.
RadioItemvaluestringRequired. Matches the group's value.
RadioItemdisabledbooleanDisables this item.

A non-interactive header inside a content panel.

PropTypeDefaultDescription
insetbooleanfalseWhen true, adds left padding to align with checkbox / radio items.

A 1 px horizontal divider with -mx-1 my-1 to extend across the panel padding.

A right-aligned <span> for the keyboard shortcut hint. Styled with ml-auto text-xs tracking-widest text-muted-foreground.

PropTypeDefaultDescription
...propsReact.ComponentProps<"span">-Standard span attributes.

Nested submenus. The sub-trigger automatically appends a right-pointing chevron.

ComponentPropTypeDescription
SubdefaultOpenbooleanUncontrolled initial open state.
SubopenbooleanControlled open state.
SubonOpenChange(open: boolean) => voidFires when the sub opens or closes.
SubTriggerinsetbooleanAdds left padding for alignment with checkbox / radio items.
SubTriggerdisabledbooleanDisables the sub-trigger.

MenubarGroup is a logical grouping wrapper (no styling); MenubarPortal is the portal Radix uses to mount content (already used internally by <MenubarContent>).

Composition

  1. <Menubar> is the horizontal bar.
  2. Each menu is a <MenubarMenu> containing a <MenubarTrigger> and a <MenubarContent>.
  3. Inside content: a mix of <MenubarItem>, <MenubarCheckboxItem>, <MenubarRadioGroup> (with <MenubarRadioItem> children), <MenubarLabel>, <MenubarSeparator>, and <MenubarSub> for nested menus.
  4. Each action item can carry a trailing <MenubarShortcut> for the keyboard hint.

The cross-menu keyboard contract is what distinguishes Menubar from a row of independent DropdownMenus: once one menu is open, ArrowLeft / ArrowRight walk to the previous / next menu's content directly, without closing and re-opening manually.

Variations

File / Edit / View with shortcuts

'use client';import {  Menubar,  MenubarContent,  MenubarItem,  MenubarMenu,  MenubarSeparator,  MenubarShortcut,  MenubarTrigger,} from '@gremorie/rx-navigation';export function MenubarShortcutsPreview() {  return (    <Menubar>      <MenubarMenu>        <MenubarTrigger>File</MenubarTrigger>        <MenubarContent>          <MenubarItem>            New tab <MenubarShortcut>CtrlT</MenubarShortcut>          </MenubarItem>          <MenubarItem>            New window <MenubarShortcut>CtrlN</MenubarShortcut>          </MenubarItem>          <MenubarSeparator />          <MenubarItem>Print...</MenubarItem>        </MenubarContent>      </MenubarMenu>      <MenubarMenu>        <MenubarTrigger>Edit</MenubarTrigger>        <MenubarContent>          <MenubarItem>            Undo <MenubarShortcut>CtrlZ</MenubarShortcut>          </MenubarItem>          <MenubarItem>            Redo <MenubarShortcut>CtrlShiftZ</MenubarShortcut>          </MenubarItem>        </MenubarContent>      </MenubarMenu>      <MenubarMenu>        <MenubarTrigger>View</MenubarTrigger>        <MenubarContent>          <MenubarItem>Reload</MenubarItem>          <MenubarItem>Fullscreen</MenubarItem>        </MenubarContent>      </MenubarMenu>    </Menubar>  );}

Use as the top-of-window bar in editor-style apps. Each menu groups related commands, and <MenubarShortcut> renders the keyboard hint right-aligned. The shortcut text is visual only - wire the real bindings in your own handler.

With checkbox and radio items

<Menubar>
  <MenubarMenu>
    <MenubarTrigger>View</MenubarTrigger>
    <MenubarContent>
      <MenubarCheckboxItem checked>Always show bookmarks</MenubarCheckboxItem>
      <MenubarCheckboxItem>Show full URL</MenubarCheckboxItem>
      <MenubarSeparator />
      <MenubarLabel inset>Density</MenubarLabel>
      <MenubarRadioGroup value="comfortable">
        <MenubarRadioItem value="compact">Compact</MenubarRadioItem>
        <MenubarRadioItem value="comfortable">Comfortable</MenubarRadioItem>
        <MenubarRadioItem value="spacious">Spacious</MenubarRadioItem>
      </MenubarRadioGroup>
    </MenubarContent>
  </MenubarMenu>
</Menubar>

Use to expose persistent view preferences. Checkbox items toggle independently; radio items pick one of N.

Nested submenus

'use client';import {  Menubar,  MenubarContent,  MenubarItem,  MenubarMenu,  MenubarSeparator,  MenubarSub,  MenubarSubContent,  MenubarSubTrigger,  MenubarTrigger,} from '@gremorie/rx-navigation';export function MenubarSubmenuPreview() {  return (    <Menubar>      <MenubarMenu>        <MenubarTrigger>File</MenubarTrigger>        <MenubarContent>          <MenubarItem>New file</MenubarItem>          <MenubarSub>            <MenubarSubTrigger>Open recent</MenubarSubTrigger>            <MenubarSubContent>              <MenubarItem>Project alpha</MenubarItem>              <MenubarItem>Project beta</MenubarItem>              <MenubarItem>Project gamma</MenubarItem>            </MenubarSubContent>          </MenubarSub>          <MenubarSeparator />          <MenubarItem variant="destructive">Close window</MenubarItem>        </MenubarContent>      </MenubarMenu>    </Menubar>  );}

Use sparingly. Submenus are valuable for grouping (Open recent, Export as, Theme), but deep nesting hurts discoverability. <MenubarSubTrigger> appends a right-pointing chevron automatically.

Accessibility

  • WAI-ARIA Menubar pattern: the root carries role="menubar"; triggers carry role="menuitem" with aria-haspopup="menu" and aria-expanded; content panels carry role="menu"; items carry role="menuitem", role="menuitemcheckbox", or role="menuitemradio" as appropriate.
  • Cross-menu keyboard: Tab enters the bar; ArrowLeft / ArrowRight walk between triggers (and continue walking between open menus' contents); ArrowDown / Enter open the active trigger; ArrowUp / ArrowDown move between items inside content; ArrowRight opens a sub-trigger; ArrowLeft closes a sub; Escape closes the open menu and returns focus to the trigger.
  • Type-ahead: Radix supports first-letter type-ahead inside each menu, so typing "S" jumps to the first item starting with S.
  • Shortcut hints: <MenubarShortcut> is visual only. Wire the actual keyboard bindings outside the component (your own document-level handler).
  • Disabled items: render with aria-disabled="true" and are skipped by arrow-key navigation.

On this page