Navigation
Menu
Dropdown menu of actions opened from a button: items with icons and shortcuts, checkbox and radio options, submenus, a search header and a metadata footer.
Installation
Install the package (React 19):
Import the styles once, in your app entry or root layout:
Use it anywhere inside your app's <Theme> (see Installation):
Examples
Checkbox, radio and submenu
Options keep their state outside the menu; submenus open on → or hover.
Search header, toggle rows and footer
header / footer slots sit outside the item list. ToggleItem is the switch row and keeps the menu open.
Static surface
The same look without behavior, for custom pickers and previews.
Composition
- Every part also has a flat alias for Server Components:
MenuRoot,MenuItem,MenuCheckboxItem… MenuSurface,MenuList,MenuRow,MenuLabel,MenuSeparatorare the same look without behavior, for pickers that manage focus themselves.
API Reference
Menu.Root
Holds the open state.
| Prop | Type | Default |
|---|---|---|
open | boolean | |
defaultOpen | boolean | false |
onOpenChange | (open: boolean) => void | |
modal | boolean | true |
Menu.Trigger
The button that opens the menu.
| Prop | Type | Default |
|---|---|---|
asChild | boolean | false |
Menu.Content
The floating menu (portalled to body).
| Prop | Type | Default |
|---|---|---|
width | number | |
header | ReactNode | |
footer | ReactNode | |
side | "top""right""bottom""left" | "bottom" |
sideOffset | number | 4 |
align | "start""center""end" | "start" |
alignOffset | number | 0 |
avoidCollisions | boolean | true |
collisionPadding | number | Partial<Record<Side, number>> | 8 |
onEscapeKeyDown | (event: KeyboardEvent) => void | |
onPointerDownOutside | (event: PointerDownOutsideEvent) => void | |
onCloseAutoFocus | (event: Event) => void | |
forceMount | true | |
loop | boolean | false |
Menu.Item
An action row.
| Prop | Type | Default |
|---|---|---|
icon | IconName | ReactNode | |
children | ReactNode | |
description | ReactNode | |
caption | ReactNode | |
shortcut | ReactNode | |
submenu | boolean | false |
checked | boolean | false |
toggle | boolean | |
trailing | ReactNode | |
tone | "default""danger""accent""muted" | "default" |
destructive | boolean | false |
disabled | boolean | false |
onSelect | (event: Event) => void | |
textValue | string |
Menu.CheckboxItem
An on/off option (menuitemcheckbox) with a trailing checkmark when checked.
| Prop | Type | Default |
|---|---|---|
icon | IconName | ReactNode | |
children | ReactNode | |
description | ReactNode | |
caption | ReactNode | |
shortcut | ReactNode | |
checked | boolean | "indeterminate" | |
onCheckedChange | (checked: boolean) => void | |
disabled | boolean | false |
onSelect | (event: Event) => void | |
textValue | string |
Menu.RadioGroup
Groups RadioItems; exactly one is selected.
| Prop | Type | Default |
|---|---|---|
value | string | |
onValueChange | (value: string) => void |
Menu.RadioItem
One option of a RadioGroup (menuitemradio), checkmark when selected.
| Prop | Type | Default |
|---|---|---|
icon | IconName | ReactNode | |
children | ReactNode | |
description | ReactNode | |
caption | ReactNode | |
shortcut | ReactNode | |
value | string | |
disabled | boolean | false |
onSelect | (event: Event) => void | |
textValue | string |
Menu.ToggleItem
Motion extra: an on/off row drawn with a switch. Keeps the menu open when toggled.
| Prop | Type | Default |
|---|---|---|
icon | IconName | ReactNode | |
children | ReactNode | |
description | ReactNode | |
caption | ReactNode | |
checked | boolean | |
onCheckedChange | (checked: boolean) => void | |
disabled | boolean | false |
onSelect | (event: Event) => void | |
textValue | string |
Menu.Sub
Wraps a submenu (SubTrigger + SubContent).
| Prop | Type | Default |
|---|---|---|
open | boolean | |
defaultOpen | boolean | false |
onOpenChange | (open: boolean) => void |
Menu.SubTrigger
Row that opens a submenu (→ or Enter opens, ← closes). Draws the chevron itself.
| Prop | Type | Default |
|---|---|---|
icon | IconName | ReactNode | |
children | ReactNode | |
description | ReactNode | |
caption | ReactNode | |
disabled | boolean | false |
textValue | string |
Menu.SubContent
The submenu surface (portalled).
| Prop | Type | Default |
|---|---|---|
width | number | |
sideOffset | number | 2 |
collisionPadding | number | 8 |
Menu.Label / Separator / Group
Section label, hairline separator and a grouping wrapper.
| Prop | Type | Default |
|---|---|---|
className | string |
Menu.Search / Menu.Footer
Search: filter field for the header slot (its accessible name defaults to the placeholder). Footer: grey metadata lines for the footer slot.
| Prop | Type | Default |
|---|---|---|
noIcon | boolean | false |
wrapperClassName | string | |
lines | ReactNode[] | |
separator | boolean | true |
MenuSurface
Static menu card.
| Prop | Type | Default |
|---|---|---|
width | number |
MenuList
Static section (role listbox; give it an aria-label) with 4px padding. A <div>: all its props pass through.
| Prop | Type | Default |
|---|---|---|
separator | boolean | false |
MenuRow
Static row (role option; a toggle row adds aria-checked). A <div>: pass tabIndex={0} to make an actionable row a Tab stop (Enter / Space click it).
| Prop | Type | Default |
|---|---|---|
icon | IconName | ReactNode | |
children | ReactNode | |
description | ReactNode | |
caption | ReactNode | |
shortcut | ReactNode | |
submenu | boolean | false |
checked | boolean | false |
toggle | boolean | |
trailing | ReactNode | |
tone | "default""danger""accent""muted" | "default" |
highlighted | boolean | false |
onToggle | (next: boolean) => void |
MenuSwitch
The purely visual switch drawn by toggle rows.
| Prop | Type | Default |
|---|---|---|
checked | boolean | false |
MenuSearch
Search field for the top of a static menu or Menu.Content header: an <input type="search"> (all input props pass through) in a 12px-padded wrapper. Its accessible name defaults to the placeholder.
| Prop | Type | Default |
|---|---|---|
noIcon | boolean | false |
wrapperClassName | string |
SearchField
The same 28px search input without the wrapper padding, for toolbars and custom pickers. All <input> props pass through.
| Prop | Type | Default |
|---|---|---|
noIcon | boolean | false |
MenuFooter
Grey metadata lines at the bottom of a menu ("130 words", "Edited today"). A <div>: all its props pass through.
| Prop | Type | Default |
|---|---|---|
lines | ReactNode[] | |
separator | boolean | true |
MenuLabel / MenuSeparator / MenuBadge
Static section label (12px, secondary), hairline separator (role="separator"), and the accent "New" pill for rows. Plain elements: all HTML props pass through.
| Prop | Type | Default |
|---|---|---|
className | string |
Accessibility
- Follows the WAI-ARIA menu button pattern: the trigger has
aria-haspopup="menu"andaria-expanded. - Enter, Space or ↓ on the trigger opens the menu and focuses the first item; ↑ opens it on the last item.
- ↑/↓ move between items (wrapping), Home/End jump to the ends, typing a letter jumps to the matching item.
- Enter or Space activates the item; → opens a submenu, ← or Escape closes it. Escape closes the menu and returns focus to the trigger.
- Checkbox and radio items expose
menuitemcheckbox/menuitemradiowitharia-checked. - Static parts:
MenuListis alistbox(give it anaria-label) ofMenuRowoptions; atogglerow is an option witharia-checked. Rows are not Tab stops by default and a click keeps focus in the field driving the list (aria-activedescendantpickers); passtabIndex={0}to make an actionable row a Tab stop that Enter / Space activate. - Content is collision-aware: it flips and shifts to stay 8px inside the viewport and scrolls when taller than the space left.
- Rows are 28px tall, above the 24px minimum target. Opens and closes with the shared motion layer (
styles/motion.css); withprefers-reduced-motion: reduceit only fades.
Usage
- Use for a short list of actions or view options attached to a button ("…", "Sort", "Actions").
- Use
CheckboxItemfor independent on/off options andRadioGroup+RadioItemfor one-of-many choices. - Mark destructive actions with
destructiveand put them last, after a separator. - Don't use a menu to pick a form value: use Select or Combobox, which work with forms and labels.
- Don't put long or searchable lists in a menu: use CommandPalette.
- Shortcut hints are display-only: register the keyboard shortcut yourself.