Menubar
When to use
Section titled “When to use”- 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:
| Component | Use it for | For example |
|---|---|---|
| Menubar | Several menus side by side, moved between as one bar | An editor’s File / Edit / View |
| Menu | One list of actions or settings opened from a button | “More actions ⋯” on a row or a card |
| Toolbar | A row of buttons, toggles and fields acting on one thing | An editor’s formatting bar |
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.
Submenus
Section titled “Submenus”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.
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> );}Vertical
Section titled “Vertical”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> );}Playground
Section titled “Playground”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
Section titled “Menubar.Root”| Prop | Type | Default | Description |
|---|---|---|---|
orientation | 'horizontal' | 'vertical' | 'horizontal' | Which arrow keys move along the bar. A vertical bar opens its menus with → and to the right. |
focusableWhenDisabled | boolean | false | Keeps disabled Triggers in the arrow-key sequence, so a screen reader announces them as unavailable. They still cannot open. |
className | string | Concatenated with the component's own classes, never replacing them. | |
render | ReactElement | Element or component to render instead of the default. Props, className, event handlers and ref are merged onto it. | |
ref | Ref<HTMLElement> | Ref to the rendered element. Merged with any ref on the render element. | |
…rest | HTMLAttributes | Anything else is spread onto the rendered element, so id, aria-*, data-* and event handlers all reach the DOM. |
Menubar.Menu
Section titled “Menubar.Menu”| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | Controlled state. Provide onOpenChange alongside it. Never pass undefined — the mode is fixed at mount. | |
defaultOpen | boolean | false | Initial state when uncontrolled. Read once, at mount. |
onOpenChange | (open: boolean) => void | Called on every request to open or close, moving along the bar included. A controlled menu moves only if you accept it. | |
focusableWhenDisabled | boolean | false | Keeps this menu's disabled items in the arrow-key sequence. They still cannot be activated. |
children | ReactNode | The Trigger and the Popup. Menu renders no element of its own. | |
className | string | Concatenated with the component's own classes, never replacing them. | |
render | ReactElement | Element or component to render instead of the default. Props, className, event handlers and ref are merged onto it. | |
ref | Ref<HTMLElement> | Ref to the rendered element. Merged with any ref on the render element. | |
…rest | HTMLAttributes | Anything else is spread onto the rendered element, so id, aria-*, data-* and event handlers all reach the DOM. |
Menubar.Popup
Section titled “Menubar.Popup”| Prop | Type | Default | Description |
|---|---|---|---|
side | 'top' | 'bottom' | 'left' | 'right' | 'bottom'; 'right' in a submenu or a vertical bar | Which 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. |
className | string | Concatenated with the component's own classes, never replacing them. | |
render | ReactElement | Element or component to render instead of the default. Props, className, event handlers and ref are merged onto it. | |
ref | Ref<HTMLElement> | Ref to the rendered element. Merged with any ref on the render element. | |
…rest | HTMLAttributes | Anything 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.