A React menu with a Liquid Glass popup, groups, separators, icons, shortcuts, and controlled open state.
Menu renders the trigger, portal, positioner, glass popup, list, action items, groups, and separators internally. Selecting an item closes the menu by default and calls its onSelect callback.
Items are passed through the items array. Use type: "group" for a labelled section and type: "separator" for a divider. Action items need a unique value for React keys. When the visible label is not plain text, pass textValue so keyboard typeahead can match it.
The example combines grouped actions, a separator, icons, shortcuts, and a disabled item. The status text below the trigger uses aria-live="polite" so screen readers hear the selected action.
No action selected.
side, align, sideOffset, and alignOffset control the popup position. The menu flips or shifts when it would overflow the viewport.
Pass open and onOpenChange to own visibility. The trigger still toggles the menu unless you also control it externally.
The menu is closed.
Menu follows the menu-button pattern: the trigger exposes aria-haspopup="menu", aria-expanded, and aria-controls; the inner list carries role="menu", and each action is a menuitem. ArrowDown and ArrowUp move through enabled items, and typeahead matches item labels. Enter or Space activates the highlighted item, and Escape closes the menu and returns focus to the trigger.
Disabled items stay announced but cannot be activated. value is only the React key. When label is not plain text, pass textValue so typeahead has a readable label, and pass aria-label or aria-labelledby when the item also needs an accessible name. See the accessibility guide for the shared interaction patterns.
Menu
| Prop | Type | Default | Description |
|---|---|---|---|
trigger | ReactElement | — | Element that opens the menu. Required. It must forward props and refs. |
items | readonly (MenuItemProps | MenuGroupProps | MenuSeparatorProps)[] | — | Action items, groups, and separators rendered by the menu. |
side | "top" | "bottom" | "left" | "right" | "inline-start" | "inline-end" | "bottom" | Preferred side of the trigger. |
align | "start" | "center" | "end" | "start" | Alignment relative to the trigger. |
sideOffset | number | OffsetFunction | 8 | Distance between the trigger and menu in pixels. |
alignOffset | number | OffsetFunction | 0 | Offset along the alignment axis in pixels. |
collisionPadding | Padding | 5px per side | Space kept between the popup and the collision boundary. |
open | boolean | — | Controlled open state. |
defaultOpen | boolean | false | Initial open state when uncontrolled. |
onOpenChange | (open: boolean, details) => void | — | Called when the menu opens or closes. |
onOpenChangeComplete | (open: boolean) => void | — | Called after the open or close animation finishes. |
modal | boolean | true | Locks outside interaction while the menu is open. |
loopFocus | boolean | true | Loops keyboard focus through the items. |
highlightItemOnHover | boolean | true | Highlights items when the pointer moves over them. |
orientation | "horizontal" | "vertical" | "vertical" | Roving focus axis used by the arrow keys. |
closeParentOnEsc | boolean | false | In a nested menu, closes the whole menu on Escape instead of only the child menu. |
onItemHighlighted | (item: HTMLElement | undefined, details) => void | — | Called when the highlighted item changes. |
actionsRef | RefObject<Menu.Root.Actions | null> | — | Imperative close and unmount actions. |
triggerId | string | null | — | ID of the trigger for a controlled menu. |
defaultTriggerId | string | null | — | ID of the trigger for an initially open menu. |
handle | Menu.Handle<Payload> | — | Handle that lets external triggers control the menu. |
disabled | boolean | false | Prevents the menu from opening. |
className | string | — | Tailwind classes merged into the popup. |
style | CSSProperties | — | Inline styles merged into the same element as className. |
ui | { positioner?: ClassValue; popup?: ClassValue; glass?: ClassValue; overlay?: ClassValue; list?: ClassValue; item?: ClassValue; itemIcon?: ClassValue; itemLabel?: ClassValue; shortcut?: ClassValue; group?: ClassValue; groupLabel?: ClassValue; separator?: ClassValue } | — | Tailwind classes for internal slots, keyed by data-slot. |
MenuItemProps
| Prop | Type | Default | Description |
|---|---|---|---|
type | "item" | — | Marks the entry as an action item. Optional when the entry has no group or separator type. |
value | string | — | Unique value used as the React key for this item. Required. |
label | ReactNode | — | Visible content inside the item. Required. |
textValue | string | — | Plain-text label used for keyboard typeahead. Defaults to a string label. |
icon | ReactNode | — | Decorative icon rendered before the label. |
shortcut | ReactNode | — | Shortcut hint rendered after the label. It is visual only. |
onSelect | (event) => void | — | Called when the item is selected. |
disabled | boolean | false | Disables the item. |
closeOnClick | boolean | true | Whether selecting the item closes the menu. |
id | string | — | ID of the item element. |
nativeButton | boolean | false | Whether the rendered element is a native <button>. |
render | ReactElement | ((props, state) => ReactElement) | — | Replaces the item element. |
className | string | — | Tailwind classes merged into the item. |
style | CSSProperties | — | Inline styles merged into the same element as className. |
ui | { item?: ClassValue; itemIcon?: ClassValue; itemLabel?: ClassValue; shortcut?: ClassValue } | — | Tailwind classes for this item's slots. |
MenuGroupProps
| Prop | Type | Default | Description |
|---|---|---|---|
type | "group" | — | Marks the entry as a group. Required. |
label | ReactNode | — | Group heading. Required. |
items | readonly MenuItemProps[] | — | Items rendered inside the group. Required. |
render | ReactElement | ((props, state) => ReactElement) | — | Replaces the group element. |
className | string | — | Tailwind classes merged into the group. |
style | CSSProperties | — | Inline styles merged into the same element as className. |
ui | { group?: ClassValue; groupLabel?: ClassValue; item?: ClassValue; itemIcon?: ClassValue; itemLabel?: ClassValue; shortcut?: ClassValue } | — | Tailwind classes for the group and its item slots. |
MenuGroupProps also forwards the remaining Menu.Group props.
MenuSeparatorProps
| Prop | Type | Default | Description |
|---|---|---|---|
type | "separator" | — | Marks the entry as a separator. Required. |
render | ReactElement | ((props, state) => ReactElement) | — | Replaces the separator element. |
className | string | — | Tailwind classes merged into the separator. |
style | CSSProperties | — | Inline styles merged into the same element as className. |
ui | { separator?: ClassValue } | — | Tailwind classes for the separator slot. |
MenuSeparatorProps also forwards Menu.Separator props such as render and id.
ui keys map to the data-slot values listed below. All other Base UI Menu.Root props are forwarded.
| Attribute | Description |
|---|---|
data-open | Present on the popup when the menu is open. |
data-closed | Present on the popup while it is closed or closing. |
data-highlighted | Present on the keyboard- or pointer-highlighted item. |
data-disabled | Present when the menu or an item is disabled. |
data-popup-open | Present on the trigger while the menu is open. |
data-side | Indicates the resolved side of the popup. |
data-align | Indicates the resolved alignment (start, center, or end). |
data-instant | Present when animations should be skipped. |
data-starting-style | Present when the popup begins animating in. |
data-ending-style | Present when the popup is animating out. |
data-slot | Marks internal parts (positioner, popup, glass, overlay, list, item, item-icon, item-label, shortcut, group, group-label, separator). |
The component uses the shared glass material. Tune shared --ui-glass-* tokens on a parent, use className for layout, and use ui for internal slots keyed by data-slot. Component-specific values live in src/theme/menu.ts. See the theming guide for token layers and presets.