Menu
A menu of actions or links opened from a button, with groups, separators, shortcuts, a critical tone, and checkbox and radio items. For choosing a form value, use Select; for navigation between views, use TabNav.
Import
import { Menu } from '@nizivo/react/menu';
// Once per app (Getting started), after
// @nizivo/tokens/tokens.css and @nizivo/react/css/base.css and @nizivo/react/css/kbd.css
import '@nizivo/react/css/menu.css';Static or interactive
Client. It always ships React Aria's menu and popup code (+61,399 B gzip under Turbopack for one Menu on the Next.js fixture). An IconButton trigger with its default tooltip adds the Tooltip code too. How to choose.
Examples
Basic
The Figma Dropdown Menu: grouped actions with shortcuts; the destructive item sits last.
import type { JSX } from 'react';
import { Button } from '@nizivo/react/button';
import { Menu } from '@nizivo/react/menu';
export function MenuExample(): JSX.Element {
return (
<Menu.Root>
<Menu.Trigger render={<Button variant="secondary">Actions</Button>} />
<Menu.Popup>
<Menu.Group>
<Menu.GroupLabel>Product</Menu.GroupLabel>
<Menu.Item shortcut="mod+e">Edit product</Menu.Item>
<Menu.Item shortcut="mod+d">Duplicate product</Menu.Item>
<Menu.Item shortcut="mod+p">Preview on store</Menu.Item>
<Menu.Item shortcut="mod+l" disabled>
Print barcode labels
</Menu.Item>
</Menu.Group>
<Menu.Separator />
<Menu.Item tone="critical" shortcut="backspace">
Delete product
</Menu.Item>
</Menu.Popup>
</Menu.Root>
);
}
Row Actions
A row's "…" menu: an IconButton as the trigger, and link items that navigate.
import type { ComponentPropsWithRef, JSX } from 'react';
import { IconButton } from '@nizivo/react/icon-button';
import { Menu } from '@nizivo/react/menu';
/** A stand-in for the `@nizivo/icons` ellipsis glyph (a 20 px Lucide stroke). */
function Ellipsis(): JSX.Element {
return (
<svg
viewBox="0 0 24 24"
width="1em"
height="1em"
fill="none"
stroke="currentColor"
strokeWidth={1.92}
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="12" cy="12" r="1" />
<circle cx="19" cy="12" r="1" />
<circle cx="5" cy="12" r="1" />
</svg>
);
}
/** A stand-in for a router's link component, such as Next's `Link`. */
function StoreLink({ children, ...props }: ComponentPropsWithRef<'a'>): JSX.Element {
return <a {...props}>{children}</a>;
}
export function MenuRowActionsExample(): JSX.Element {
return (
<Menu.Root>
<Menu.Trigger
render={<IconButton label="More actions" icon={<Ellipsis />} variant="tertiary" />}
/>
<Menu.Popup align="end">
<Menu.Item href="#order-1001">Open order</Menu.Item>
<Menu.Item render={<StoreLink href="#order-1001-invoice" />}>Download invoice</Menu.Item>
<Menu.Separator />
<Menu.Item tone="critical">Cancel order</Menu.Item>
</Menu.Popup>
</Menu.Root>
);
}
Checkbox
Checkbox and radio items: the menu stays open while columns are toggled.
import type { JSX } from 'react';
import { Button } from '@nizivo/react/button';
import { Menu } from '@nizivo/react/menu';
export function MenuCheckboxExample(): JSX.Element {
return (
<Menu.Root>
<Menu.Trigger render={<Button variant="secondary">View</Button>} />
<Menu.Popup>
<Menu.Group>
<Menu.GroupLabel>Columns</Menu.GroupLabel>
<Menu.CheckboxItem defaultChecked>Status</Menu.CheckboxItem>
<Menu.CheckboxItem defaultChecked>Inventory</Menu.CheckboxItem>
<Menu.CheckboxItem>Vendor</Menu.CheckboxItem>
</Menu.Group>
<Menu.Separator />
<Menu.RadioGroup defaultValue="comfortable">
<Menu.GroupLabel>Density</Menu.GroupLabel>
<Menu.RadioItem value="comfortable">Comfortable</Menu.RadioItem>
<Menu.RadioItem value="compact">Compact</Menu.RadioItem>
</Menu.RadioGroup>
</Menu.Popup>
</Menu.Root>
);
}
Open
Open on the server: the trigger reports it, and the popup renders after hydration (§12.2 SSR).
Source only
defaultOpen), which would move focus on this page, so it is shown as source only.import type { JSX } from 'react';
import { Menu } from '@nizivo/react/menu';
export function MenuOpenExample(): JSX.Element {
return (
<Menu.Root defaultOpen>
<Menu.Trigger>Sort</Menu.Trigger>
<Menu.Popup>
<Menu.Item>Newest first</Menu.Item>
<Menu.Item>Oldest first</Menu.Item>
</Menu.Popup>
</Menu.Root>
);
}
Parts
Use the parts through the namespace (Menu.Root) or by their flat names (MenuRoot); both work from Server Components.
Menu.Root
Client component Renders no element of its own. Props: MenuRootProps. Source: src/menu/menu.client.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
childrenrequired | ReactNode | – | |
defaultOpen | boolean | – | |
onOpenChange | (open: boolean, details: ChangeDetails<OpenReason>) => void | – |
|
open | boolean | – |
Menu.Trigger
Client component Renders data-slot="menu-trigger" with the class nz-menu-trigger. Props: MenuTriggerProps. Source: src/menu/menu.client.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
children | ReactNode | – | |
render | RenderProp<MenuTriggerState> | – | Must render a |
- Also takes every attribute of
<button>exceptchildren. (Omit<ComponentPropsWithRef<"button">, "children">)
render takes an element, or, in client code, a function of the props and this state (MenuTriggerState):
| Name | Type | Default | Description |
|---|---|---|---|
disabledrequired | boolean | – | |
focusVisiblerequired | boolean | – | |
openrequired | boolean | – | |
pressedrequired | boolean | – |
Menu.Popup
Client component Renders data-slot="menu-popup" with the class nz-menu-popup. Props: MenuPopupProps. Source: src/menu/menu.client.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
align | Align"start""center""end" | 'start' | |
aria-label | string | – | Names the menu when the trigger's text does not (the trigger names it by default). |
childrenrequired | ReactNode | – | |
className | string | – | |
container | Element | null | – | Portal container. Defaults to the parent overlay, else |
side | MenuSide"top""bottom""start""end" | 'bottom' | |
sideOffset | number | 8 | Distance from the trigger in px. |
style | CSSProperties | – |
Menu.Item
Client component Renders data-slot="menu-item" with the class nz-menu-item. Props: MenuItemProps. Source: src/menu/menu.client.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
childrenrequired | ReactNode | – | From |
closeOnSelect | boolean | true (false for a checkbox item) | Whether activating the item closes the menu. From |
disabled | boolean | – | From |
download | boolean | string | – | |
href | string | – | Makes the item a link item: it renders an |
hrefLang | string | – | |
icon | ReactNode | – | A leading glyph (16 px), for example From |
onAction | () => void | – | Called when the item is activated by pointer, touch or keyboard (Enter or Space). From |
ping | string | – | |
referrerPolicy | HTMLAttributeReferrerPolicy"""no-referrer""no-referrer-when-downgrade""origin""origin-when-cross-origin""same-origin""strict-origin""strict-origin-when-cross-origin""unsafe-url" | – | |
rel | string | – | |
render | RenderProp<MenuItemState> | – | Renders a |
shortcut | string | – | A keyboard shortcut drawn at the end with From |
target | HTMLAttributeAnchorTarget | – | |
textValue | string | – | Typeahead text; defaults to From |
tone | MenuItemTone"neutral""critical" | 'neutral' | From |
- Also takes every attribute of
<div>exceptchildren,id. (Omit<ComponentPropsWithRef<"div">, "children" | "id">)
render takes an element, or, in client code, a function of the props and this state (MenuItemState):
| Name | Type | Default | Description |
|---|---|---|---|
checkedrequired | boolean | – | Checkbox and radio items only; always |
disabledrequired | boolean | – | |
focusVisiblerequired | boolean | – | |
highlightedrequired | boolean | – |
Menu.CheckboxItem
Client component Renders data-slot="menu-checkbox-item" with the class nz-menu-checkbox-item. Props: MenuCheckboxItemProps. Source: src/menu/menu.client.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
checked | boolean | – | |
childrenrequired | ReactNode | – | From |
closeOnSelect | boolean | true (false for a checkbox item) | Whether activating the item closes the menu. From |
defaultChecked | boolean | – | The popup unmounts on close, so the Root keeps an uncontrolled item's state, keyed by the item's text ( |
disabled | boolean | – | From |
icon | ReactNode | – | A leading glyph (16 px), for example From |
indicator | ReactNode | – | Replaces the check glyph shown while checked. |
onAction | () => void | – | Called when the item is activated by pointer, touch or keyboard (Enter or Space). From |
onCheckedChange | (checked: boolean, details: ChangeDetails<MenuCheckedReason>) => void | – | |
render | RenderProp<MenuItemState> | – | Must render a |
shortcut | string | – | A keyboard shortcut drawn at the end with From |
textValue | string | – | Typeahead text; defaults to From |
tone | MenuItemTone"neutral""critical" | 'neutral' | From |
- Also takes every attribute of
<div>exceptchildren,id. (Omit<ComponentPropsWithRef<"div">, "children" | "id">)
render takes an element, or, in client code, a function of the props and this state (MenuItemState):
| Name | Type | Default | Description |
|---|---|---|---|
checkedrequired | boolean | – | Checkbox and radio items only; always |
disabledrequired | boolean | – | |
focusVisiblerequired | boolean | – | |
highlightedrequired | boolean | – |
Menu.RadioGroup
Client component Renders data-slot="menu-radio-group" with the class nz-menu-radio-group. Props: MenuRadioGroupProps. Source: src/menu/menu.client.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
defaultValue | string | null | – | The popup unmounts on close, so the Root keeps an uncontrolled group's choice, keyed by its items' |
onValueChange | (value: string, details: ChangeDetails<MenuCheckedReason>) => void | – |
|
render | RenderProp<Record<string, never>> | – | Must render a |
value | string | null | – |
- Also takes every attribute of
<section>exceptdefaultValue,id. (Omit<ComponentPropsWithRef<"section">, "id" | "defaultValue">)
render takes an element, or, in client code, a function of the props and no state.
Menu.RadioItem
Client component Renders data-slot="menu-radio-item" with the class nz-menu-radio-item. Props: MenuRadioItemProps. Source: src/menu/menu.client.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
childrenrequired | ReactNode | – | From |
closeOnSelect | boolean | true (false for a checkbox item) | Whether activating the item closes the menu. From |
disabled | boolean | – | From |
icon | ReactNode | – | A leading glyph (16 px), for example From |
indicator | ReactNode | – | Replaces the check glyph shown while chosen. |
onAction | () => void | – | Called when the item is activated by pointer, touch or keyboard (Enter or Space). From |
render | RenderProp<MenuItemState> | – | Must render a |
shortcut | string | – | A keyboard shortcut drawn at the end with From |
textValue | string | – | Typeahead text; defaults to From |
tone | MenuItemTone"neutral""critical" | 'neutral' | From |
valuerequired | string | – | The value the group reports when this item is chosen. |
- Also takes every attribute of
<div>exceptchildren,id. (Omit<ComponentPropsWithRef<"div">, "children" | "id">)
render takes an element, or, in client code, a function of the props and this state (MenuItemState):
| Name | Type | Default | Description |
|---|---|---|---|
checkedrequired | boolean | – | Checkbox and radio items only; always |
disabledrequired | boolean | – | |
focusVisiblerequired | boolean | – | |
highlightedrequired | boolean | – |
Menu.Group
Client component Renders data-slot="menu-group" with the class nz-menu-group. Props: MenuGroupProps. Source: src/menu/menu.client.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
render | RenderProp<NoState> | – | Must render a |
- Also takes every attribute of
<section>exceptid. (Omit<ComponentPropsWithRef<"section">, "id">)
render takes an element, or, in client code, a function of the props and no state.
Menu.GroupLabel
Client component Renders data-slot="menu-group-label" with the class nz-menu-group-label. Props: MenuGroupLabelProps. Source: src/menu/menu.client.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
render | RenderProp<NoState> | – | Must render a |
- Also takes every attribute of
<header>exceptid. (Omit<ComponentPropsWithRef<"header">, "id">)
render takes an element, or, in client code, a function of the props and no state.
Menu.Separator
Client component Renders data-slot="menu-separator" with the class nz-menu-separator. Props: MenuSeparatorProps. Source: src/menu/menu.client.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
render | RenderProp<NoState> | – | Must render a |
- Also takes every attribute of
<div>exceptchildren. (Omit<ComponentPropsWithRef<"div">, "children">)
render takes an element, or, in client code, a function of the props and no state.
Other exports
MenuCheckboxItemPropstypeimport type { MenuCheckboxItemProps } from '@nizivo/react/menu'The props of
Menu.CheckboxItem, listed above.MenuCheckedReasontypeimport type { MenuCheckedReason } from '@nizivo/react/menu'Why a checkbox item changed: the item was activated (pointer, touch or keyboard).
type MenuCheckedReason = "item-press"MenuGroupLabelPropstypeimport type { MenuGroupLabelProps } from '@nizivo/react/menu'The props of
Menu.GroupLabel, listed above.MenuGroupPropstypeimport type { MenuGroupProps } from '@nizivo/react/menu'The props of
Menu.Group, listed above.MenuItemPropstypeimport type { MenuItemProps } from '@nizivo/react/menu'The props of
Menu.Item, listed above.MenuItemStatetypeimport type { MenuItemState } from '@nizivo/react/menu'The render state of
Menu.CheckboxItem, listed above.MenuItemTonetypeimport type { MenuItemTone } from '@nizivo/react/menu'Figma Menu Item
State=Critical.criticalitems sit last, after a separator.type MenuItemTone = "neutral" | "critical"MenuLabelstypeimport type { MenuLabels } from '@nizivo/react/menu'Strings the engine renders (§9.3 rule 7). Engine-owned with React Aria (not overridable):
dismiss, the visually hidden "Dismiss" buttons at both ends of the popup.Name Type Default Description dismissrequiredstring– MenuPopupPropstypeimport type { MenuPopupProps } from '@nizivo/react/menu'The props of
Menu.Popup, listed above.MenuPopupStatetypeimport type { MenuPopupState } from '@nizivo/react/menu'Name Type Default Description enteringrequiredboolean– exitingrequiredboolean– siderequiredSide"top""right""bottom""left"– MenuRadioGroupPropstypeimport type { MenuRadioGroupProps } from '@nizivo/react/menu'The props of
Menu.RadioGroup, listed above.MenuRadioItemPropstypeimport type { MenuRadioItemProps } from '@nizivo/react/menu'The props of
Menu.RadioItem, listed above.MenuRootPropstypeimport type { MenuRootProps } from '@nizivo/react/menu'The props of
Menu.Root, listed above.MenuSeparatorPropstypeimport type { MenuSeparatorProps } from '@nizivo/react/menu'The props of
Menu.Separator, listed above.MenuSidetypeimport type { MenuSide } from '@nizivo/react/menu'Logical side; the resolved physical side is
data-sideon the popup.type MenuSide = "top" | "bottom" | "start" | "end"MenuTriggerPropstypeimport type { MenuTriggerProps } from '@nizivo/react/menu'The props of
Menu.Trigger, listed above.MenuTriggerStatetypeimport type { MenuTriggerState } from '@nizivo/react/menu'The render state of
Menu.Trigger, listed above.
Data attributes and slots
Style parts by class (.nz-<slot>) and state by data attribute. Attribute meanings come from contracts/vocabulary.json.
| Slot | Rendered by | Data attributes |
|---|---|---|
menu-checkbox-item.nz-menu-checkbox-item | Menu.CheckboxItemroot (takes render); interactive, focus: engine; forced colours: highlight on [data-checked] |
|
menu-group.nz-menu-group | Menu.Grouproot (takes render) | none |
menu-group-label.nz-menu-group-label | Menu.GroupLabelroot (takes render) | none |
menu-item.nz-menu-item | Menu.Itemroot (takes render); interactive, focus: engine |
|
menu-list.nz-menu-list | Menu.Popupinner element (CSS only) | none |
menu-popup.nz-menu-popup | Menu.Popuproot (no render); forced colours: border |
|
menu-radio-group.nz-menu-radio-group | Menu.RadioGrouproot (takes render) | none |
menu-radio-item.nz-menu-radio-item | Menu.RadioItemroot (takes render); interactive, focus: engine; forced colours: highlight on [data-checked] |
|
menu-separator.nz-menu-separator | Menu.Separatorroot (takes render) | none |
menu-trigger.nz-menu-trigger | Menu.Triggerroot (takes render); interactive, focus: self |
|
CSS hooks
Set a hook on the component's class or any ancestor to restyle it without specificity games; leave it unset for the default shown.
| Hook | Default | Read as |
|---|---|---|
--nz-menu-bg | --nz-bg-surface-raised |
|
--nz-menu-border | --nz-border-subtle |
|
--nz-menu-radius | --nz-radius-lg |
|
--nz-menu-shadow | --nz-elevation-3 |
|
Layers: nz.components. Classes: .nz-menu-checkbox-item, .nz-menu-group, .nz-menu-group-label, .nz-menu-item, .nz-menu-item-icon, .nz-menu-item-indicator, .nz-menu-item-label, .nz-menu-item-shortcut, .nz-menu-list, .nz-menu-popup, .nz-menu-radio-group, .nz-menu-radio-item, .nz-menu-separator. Runtime variables: --nz-available-height, --nz-transform-origin, --nz-trigger-width.
Tokens this stylesheet reads (34)
--nz-bg-brand-subtle, --nz-bg-hover, --nz-bg-surface-raised, --nz-border-subtle, --nz-border-width-default, --nz-border-width-focus, --nz-duration-fade, --nz-duration-spring, --nz-easing-enter, --nz-easing-exit, --nz-easing-spring, --nz-elevation-3, --nz-font-body-m, --nz-font-label-m, --nz-icon-brand, --nz-icon-critical, --nz-icon-secondary, --nz-radius-lg, --nz-radius-sm, --nz-scale-enter, --nz-size-icon-sm, --nz-space-0-5, --nz-space-1, --nz-space-1-5, --nz-space-2, --nz-space-3, --nz-text-brand, --nz-text-critical, --nz-text-disabled, --nz-text-primary, --nz-text-tertiary, --nz-tracking-body-m, --nz-tracking-label-m, --nz-z-overlay
Accessibility
These parts are interactive. Each gets the focus outline and touch-action rule from @nizivo/react/css/base.css (the nz.base layer), so the floor survives a restyle:
menu-checkbox-item: draws the focus outline when the engine reports keyboard focus, not on pointer hover.menu-item: draws the focus outline when the engine reports keyboard focus, not on pointer hover.menu-radio-item: draws the focus outline when the engine reports keyboard focus, not on pointer hover.menu-trigger: draws the focus outline itself on keyboard focus.
In forced-colours mode these states keep a visible system colour: menu-checkbox-item[data-checked] (highlight), menu-popup (border), menu-radio-item[data-checked] (highlight).
Behaviour comes from the react-aria engine through Nizivo's adapter: focus management, ARIA wiring and keyboard handling are the engine's.
Pattern: WAI-ARIA Authoring Practices.
Keyboard
| Keys | Action |
|---|---|
| Enter, Space, ArrowDown | On the trigger, opens the menu and focuses the first item. |
| ArrowUp | On the trigger, opens the menu and focuses the last item. |
| ArrowDown, ArrowUp | In the open menu, moves focus to the next or previous item, wrapping at the ends and skipping disabled items. |
| Home, End | In the open menu, moves focus to the first or last item. |
| Type a character | Moves focus to the next item whose label starts with what you typed. |
| Enter, Space | Activates the focused item and closes the menu, or follows it when it is a link. A checkbox item toggles and the menu stays open, on Enter as well as Space (APG closes on Enter; a builder choice on the Deviations page); a radio item is checked and the menu closes. |
| Escape | Closes the menu and returns focus to the trigger. |
| Tab, Shift+Tab | Closes the menu and moves focus to the element after the trigger, or with Shift+Tab to the element before it. |
Source: react-aria-components 1.22.1 MenuTrigger, Popover and Menu (react-aria 3.53.1 useMenuTrigger, useMenu and useMenuItem) through _engines/react-aria/menu.client.tsx. React Aria keeps Tab inside its modal popover, so the adapter closes the menu on Tab and Shift+Tab and moves focus past the trigger (useTabOut). React Aria also renders a visually hidden Dismiss button at each end of the popup for screen-reader users; its text is the engine's own string. The overlay lane (test/overlay/menu.spec.tsx) checks every key above in Chromium and WebKit.
Figma
Node ids from design/component-map.json, which nz gen writes from component.json. The links open the Nizivo Figma library, which may ask you to sign in.
- Canonical: Figma node
7275:11516 - Ghost masters: Figma node
746:203
KpiTile
One number, its change and its shape, on Card's surface. Tint a row of related KPIs with tone, which no longer follows the trend. The Suspense fallback is the same tile with status="loading".
Meter
A native meter for a measurement within a known range, such as storage used, with low, high and optimum thresholds.