Skip to content

Menubar

  • An application’s menus along the top of an editor or a tool: File, Edit, View
  • A side rail of menus in a workspace

Not for a single “More actions ⋯” button, which is a Menu, nor for a row of buttons, which is a Toolbar.

Choosing between menus and rows of controls:

ComponentUse it forFor example
MenubarSeveral menus side by side, moved between as one barAn editor’s File / Edit / View
MenuOne list of actions or settings opened from a button“More actions ⋯” on a row or a card
ToolbarA row of buttons, toggles and fields acting on one thingAn editor’s formatting bar
Chose: nothing yet · Word wrap on · Zoom 100%
import { useState } from 'react';
import { Menubar } from '@arun-dev/ui';
export default function MenubarBasics() {
const [last, setLast] = useState('nothing yet');
const [wrap, setWrap] = useState(true);
const [zoom, setZoom] = useState('100%');
return (
<div style={{ display: 'grid', gap: 'var(--space-sm)', justifyItems: 'start' }}>
<Menubar.Root aria-label="Editor">
<Menubar.Menu>
<Menubar.Trigger>File</Menubar.Trigger>
<Menubar.Popup>
<Menubar.Item onClick={() => setLast('New')}>New</Menubar.Item>
<Menubar.Item onClick={() => setLast('Open')}>Open</Menubar.Item>
<Menubar.SubmenuRoot>
<Menubar.SubmenuTrigger>Export as</Menubar.SubmenuTrigger>
<Menubar.Popup>
<Menubar.Item onClick={() => setLast('PDF')}>PDF</Menubar.Item>
<Menubar.Item onClick={() => setLast('Markdown')}>Markdown</Menubar.Item>
</Menubar.Popup>
</Menubar.SubmenuRoot>
<Menubar.Item onClick={() => setLast('Print')}>Print</Menubar.Item>
</Menubar.Popup>
</Menubar.Menu>
<Menubar.Menu>
<Menubar.Trigger>Edit</Menubar.Trigger>
<Menubar.Popup>
<Menubar.Item onClick={() => setLast('Undo')}>Undo</Menubar.Item>
<Menubar.Item onClick={() => setLast('Redo')}>Redo</Menubar.Item>
<Menubar.Item disabled>Paste</Menubar.Item>
</Menubar.Popup>
</Menubar.Menu>
<Menubar.Menu>
<Menubar.Trigger>View</Menubar.Trigger>
<Menubar.Popup>
<Menubar.CheckboxItem checked={wrap} onCheckedChange={setWrap}>
<Menubar.ItemIndicator />
Word wrap
</Menubar.CheckboxItem>
<Menubar.RadioGroup value={zoom} onValueChange={setZoom}>
<Menubar.GroupLabel>Zoom</Menubar.GroupLabel>
{['75%', '100%', '150%'].map((level) => (
<Menubar.RadioItem key={level} value={level}>
<Menubar.ItemIndicator />
{level}
</Menubar.RadioItem>
))}
</Menubar.RadioGroup>
</Menubar.Popup>
</Menubar.Menu>
<Menubar.Menu>
<Menubar.Trigger disabled>Help</Menubar.Trigger>
<Menubar.Popup>
<Menubar.Item>About</Menubar.Item>
</Menubar.Popup>
</Menubar.Menu>
</Menubar.Root>
<span>
Chose: {last} · Word wrap {wrap ? 'on' : 'off'} · Zoom {zoom}
</span>
</div>
);
}

Menubar follows the WAI-ARIA menubar pattern. The bar is role="menubar" and one Tab stop; each Menubar.Trigger is one of its items.

  • ← and → move along the bar, wrapping; Home and End go to its ends. Typing moves to the Trigger whose text starts with what was typed.
  • ↓, Enter and Space open a menu at its first item; ↑ opens it at its last.
  • Inside an open menu, → and ← move to the next and previous menu and open it. A SubmenuTrigger’s → opens its submenu first, and inside a submenu ← closes it first.
  • While a menu is open, moving the pointer onto another Trigger opens that menu instead.
  • Esc, a click outside and Tab close the menu, as for Menu.

In a right-to-left bar ← and → swap. Disabled Triggers are skipped, unless the Root sets focusableWhenDisabled. Name the bar with aria-label.

The parts are Menu’s, under the bar’s name: Menubar.Menu holds one menu’s state, as Menu.Root does, and Menubar.Popup, Menubar.Item, Menubar.CheckboxItem, Menubar.RadioGroup, Menubar.RadioItem, Menubar.ItemIndicator, Menubar.Group, Menubar.GroupLabel, Menubar.SubmenuRoot and Menubar.SubmenuTrigger work as on Menu.

Menus nest to any depth with Menubar.SubmenuRoot and Menubar.SubmenuTrigger, as on Menu: → (or resting the pointer on it) opens a submenu, ← closes it, and Esc closes one level at a time. → on an item that opens nothing moves along the bar to the next menu, closing every level.

Chose: nothing yet
import { useState } from 'react';
import { Menubar } from '@arun-dev/ui';
export default function MenubarSubmenus() {
const [last, setLast] = useState('nothing yet');
return (
<div style={{ display: 'grid', gap: 'var(--space-sm)', justifyItems: 'start' }}>
<Menubar.Root aria-label="Document">
<Menubar.Menu>
<Menubar.Trigger>File</Menubar.Trigger>
<Menubar.Popup>
<Menubar.Item onClick={() => setLast('New')}>New</Menubar.Item>
<Menubar.SubmenuRoot>
<Menubar.SubmenuTrigger>Share</Menubar.SubmenuTrigger>
<Menubar.Popup>
<Menubar.Item onClick={() => setLast('Email')}>Email</Menubar.Item>
<Menubar.Item onClick={() => setLast('Copy link')}>Copy link</Menubar.Item>
<Menubar.SubmenuRoot>
<Menubar.SubmenuTrigger>Export as</Menubar.SubmenuTrigger>
<Menubar.Popup>
<Menubar.Item onClick={() => setLast('PDF')}>PDF</Menubar.Item>
<Menubar.Item onClick={() => setLast('PNG')}>PNG</Menubar.Item>
</Menubar.Popup>
</Menubar.SubmenuRoot>
</Menubar.Popup>
</Menubar.SubmenuRoot>
<Menubar.Item onClick={() => setLast('Print')}>Print</Menubar.Item>
</Menubar.Popup>
</Menubar.Menu>
<Menubar.Menu>
<Menubar.Trigger>Edit</Menubar.Trigger>
<Menubar.Popup>
<Menubar.Item onClick={() => setLast('Undo')}>Undo</Menubar.Item>
<Menubar.SubmenuRoot>
<Menubar.SubmenuTrigger>Find</Menubar.SubmenuTrigger>
<Menubar.Popup>
<Menubar.Item onClick={() => setLast('Find…')}>Find…</Menubar.Item>
<Menubar.Item onClick={() => setLast('Replace…')}>Replace…</Menubar.Item>
</Menubar.Popup>
</Menubar.SubmenuRoot>
</Menubar.Popup>
</Menubar.Menu>
</Menubar.Root>
<span>Chose: {last}</span>
</div>
);
}

With orientation="vertical", ↑ and ↓ move along the bar, → opens a menu beside it, and ← closes it back to its Trigger. The menus open to the right. In a right-to-left page, pass side="left" to each Menubar.Popup; side stays physical, as on every popup.

import { Menubar } from '@arun-dev/ui';
export default function MenubarVertical() {
return (
<Menubar.Root aria-label="Workspace" orientation="vertical">
<Menubar.Menu>
<Menubar.Trigger>Projects</Menubar.Trigger>
<Menubar.Popup>
<Menubar.Item>New project</Menubar.Item>
<Menubar.Item>Import</Menubar.Item>
</Menubar.Popup>
</Menubar.Menu>
<Menubar.Menu>
<Menubar.Trigger>Team</Menubar.Trigger>
<Menubar.Popup>
<Menubar.Item>Invite</Menubar.Item>
<Menubar.Item>Members</Menubar.Item>
</Menubar.Popup>
</Menubar.Menu>
<Menubar.Menu>
<Menubar.Trigger>Settings</Menubar.Trigger>
<Menubar.Popup>
<Menubar.Item>Profile</Menubar.Item>
<Menubar.Item>Billing</Menubar.Item>
</Menubar.Popup>
</Menubar.Menu>
</Menubar.Root>
);
}
Chose: nothing yet

Tab to the bar and use the arrow keys. Open File, then press → on Share and Export as to go deeper, and → on an item that opens nothing to move to Edit. Help is disabled: with focusableWhenDisabled the arrows reach it, but it never opens.

<Menubar.Root orientation="horizontal"><Menubar.Menu /> …</Menubar.Root>
Menubar.Root props
PropTypeDefaultDescription
orientation'horizontal' | 'vertical''horizontal'Which arrow keys move along the bar. A vertical bar opens its menus with → and to the right.
focusableWhenDisabledbooleanfalseKeeps disabled Triggers in the arrow-key sequence, so a screen reader announces them as unavailable. They still cannot open.
classNamestringConcatenated with the component's own classes, never replacing them.
renderReactElementElement or component to render instead of the default. Props, className, event handlers and ref are merged onto it.
refRef<HTMLElement>Ref to the rendered element. Merged with any ref on the render element.
…restHTMLAttributesAnything else is spread onto the rendered element, so id, aria-*, data-* and event handlers all reach the DOM.
Menubar.Menu props
PropTypeDefaultDescription
openbooleanControlled state. Provide onOpenChange alongside it. Never pass undefined — the mode is fixed at mount.
defaultOpenbooleanfalseInitial state when uncontrolled. Read once, at mount.
onOpenChange(open: boolean) => voidCalled on every request to open or close, moving along the bar included. A controlled menu moves only if you accept it.
focusableWhenDisabledbooleanfalseKeeps this menu's disabled items in the arrow-key sequence. They still cannot be activated.
childrenReactNodeThe Trigger and the Popup. Menu renders no element of its own.
classNamestringConcatenated with the component's own classes, never replacing them.
renderReactElementElement or component to render instead of the default. Props, className, event handlers and ref are merged onto it.
refRef<HTMLElement>Ref to the rendered element. Merged with any ref on the render element.
…restHTMLAttributesAnything else is spread onto the rendered element, so id, aria-*, data-* and event handlers all reach the DOM.
Menubar.Popup props
PropTypeDefaultDescription
side'top' | 'bottom' | 'left' | 'right''bottom'; 'right' in a submenu or a vertical barWhich side of the Trigger to open on. Flips to the opposite side when there is no room.
align'start' | 'center' | 'end''start'Flush with the Trigger's start or end edge, or centred on it.
classNamestringConcatenated with the component's own classes, never replacing them.
renderReactElementElement or component to render instead of the default. Props, className, event handlers and ref are merged onto it.
refRef<HTMLElement>Ref to the rendered element. Merged with any ref on the render element.
…restHTMLAttributesAnything else is spread onto the rendered element, so id, aria-*, data-* and event handlers all reach the DOM.

The items take the same props as on Menu.