Skip to content

Menu

A list of actions that appears when something is pressed. It nests, it holds state, and it can be driven end to end from the keyboard.

tsx
import { Button, Menu, MenuItem, MenuSeparator, MenuSubmenu } from 'neba';

<Menu trigger={<Button>Actions</Button>}>
  <MenuItem shortcut="⌘E">Rename</MenuItem>
  <MenuSubmenu label="Move to">
    <MenuItem>Archive</MenuItem>
  </MenuSubmenu>
  <MenuSeparator />
  <MenuItem color="danger">Delete</MenuItem>
</Menu>;

Props

PropTypeDefaultDescription
triggerReactElementThe element that opens the menu, wired up by Base UI. Not needed for a controlled menu opened elsewhere
openbooleanWhether it is open, for a controlled menu
defaultOpenbooleanfalseWhether it starts open
onOpenChange(open: boolean) => voidCalled when it opens or closes
sideshared'top' | 'right' | 'bottom' | 'left''bottom'Which edge of the trigger it hangs off. Flips when there is no room
alignshared'start' | 'center' | 'end''start'Where it sits along that edge
sideOffsetnumber6Distance from the trigger, in pixels
modalbooleantrueWhether the page behind is taken away while it is open
openOnHoverbooleanfalseOpens on hover too. For a menu bar, where crossing the row should walk through the others
loopFocusbooleantrueWhether the arrow keys wrap from the last row back to the first
disabledbooleanfalseUnavailable. The trigger stops opening anything
sizeshared'xs' | 'sm' | 'md' | 'lg' | 'xl''md'Height and type scale
colorshared'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info''primary'Semantic colour role. Arbitrary colour values are not accepted
densityshared'default' | 'compact''default'The rows’ padding only. A menu is as wide as its longest label, so it runs a tighter ladder than Box
childrenReactNodeThe rows — MenuItem, MenuGroup, MenuSeparator, MenuSubmenu and the rest
PropTypeDefaultDescription
onClick(event) => voidWhat the row does
hrefstringPassing it renders a real anchor. A menu of links has to be links, or none of them opens in a new tab
startIconReactNodeContent before the label
endIconReactNodeContent after the label, before any shortcut
shortcutReactNodeThe keystroke that does the same thing, set muted at the end. Shown, never bound
descriptionReactNodeA second line under the label
colorshared'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info'Re-points this row's colour family — danger for the one that deletes. Defaults to the menu's own
closeOnClickbooleantrueWhether picking the row closes the menu
disabledbooleanfalseUnavailable. Still listed, and still found by typeahead
labelstringWhat typeahead matches against, when the label is not a plain string
childrenReactNodeThe label
PropTypeDefaultDescription
labelReactNodeThe label on the row that opens it
startIconReactNodeContent before the label
sideshared'top' | 'right' | 'bottom' | 'left''right'Which edge of the parent row it opens against
sideOffsetnumber4Distance from the parent menu, in pixels
disabledbooleanfalseUnavailable
childrenReactNodeThe nested rows — one of which may be another MenuSubmenu, to any depth

ContextMenu

PropTypeDefaultDescription
contentReactNodeThe rows, written exactly as they are inside a Menu
children * ReactNodeThe area that answers a right-click or a long press
openbooleanWhether it is open
defaultOpenbooleanfalseWhether it starts open
onOpenChange(open: boolean) => voidCalled when it opens or closes
sizeshared'xs' | 'sm' | 'md' | 'lg' | 'xl''md'Height and type scale
colorshared'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info''primary'Semantic colour role. Arbitrary colour values are not accepted
densityshared'default' | 'compact''default'The rows’ padding only

Examples

Passing href turns the row into a real <a>. That is not a detail: a menu of links that are not links cannot be opened in a new tab, cannot have its address copied, and tells a screen reader the wrong thing about every row in it.

Nested menus

There is no depth limit. A submenu's children are just menu rows, and one of them can be another MenuSubmenu. Base UI opens them on hover with a safe triangle, so reaching diagonally into an open submenu does not close it.

Rows that hold state

A tick says "and", a dot says "instead of" — the same distinction Checkbox and Radio make everywhere else in the library. Neither closes the menu when it is picked: a list of things to tick is a list you tick more than one of.

Context menus

Sizes

The rows are code, not data

Select takes an items array. A menu does not. That is the opposite choice, and it is deliberate.

A select's options are values from a list the caller already has. A menu's rows are code — each one a different handler, a different icon, sometimes a submenu. Data would mean an items type with a variant for every shape a row can take, which is a component tree spelled as a discriminated union.

The popup is the Select popup

To the pixel. A select is a menu that remembers what you picked, and two floating lists of rows that do not match are two lists the eye has to learn separately.

The rows' padding is the one thing that differs. A List row spans a sheet that something else decided the width of; a menu row is inside a popup that is exactly as wide as its longest label. At md, Box's px-4 would add 32px to a menu that says "Cut", which is how a five-item menu ends up the width of a dialog.

What Base UI owns

Everything that makes a menu a menu rather than a floating list of divs: roving focus with the arrow keys, Home and End, typeahead, Escape, closing on an outside click, restoring focus to the trigger, submenus opening on hover with the safe triangle, and the menu / menuitem roles that make any of it mean something to a screen reader.

What is here is the surface, the ladders and the row layout.

Accessibility

  • Give the row that deletes color="danger". The whole colour family re-points, so the text, the soft background and the focus ring turn over together.
  • When the label is not a plain string, give label the text typeahead should match against.
  • A disabled row stays listed and stays findable by typeahead. Not disappearing is the point — a row that is gone says "there is no such thing" rather than "it is not available here".

Released under the MIT License