Skip to main content

Menu

Headless UI Menu with the ZUI popover surface. The trigger is a regular Button, so every variant, size and loading applies. Use it for actions; for picking a value use a Listbox.

import Menu from '@zuilib/components/menu'

Basic

Menu.Button defaults to variant="outline" with a trailing chevron. Menu.Items floats at bottom end and is portalled to <body>.

Loading example

Sections, icons and shortcuts

Menu.Section groups rows under a Menu.Heading; Menu.Separator draws a rule. icon sits before the label and shortcut at the end as a muted <kbd> (pass a <Kbd> for the chip look). destructive paints the row.

Loading example

Trigger variants and sizes

Every Button prop works on the trigger. While open each variant shows its pressed surface. size="icon" drops the chevron and needs an aria-label.

Loading example

Menu.Item is polymorphic: as="a" href types the anchor, as={Link} to the router. anchor moves the panel; anchor={false} renders it in place so subtree token overrides reach it.

Loading example

Props

MenuProps extends div attributes with:

PropTypeDescription
childrenReactNode | ({open, close}) => ReactElementMenu.Button + Menu.Items, or a render function
classNamestringMerged last onto the root

Every Button prop except as, plus:

PropTypeDefaultDescription
variantButtonVariant'outline'
size'sm' | 'md' | 'lg' | 'icon''md'icon needs an aria-label
hideChevronbooleanfalseNever drawn on an icon button or beside a custom trailingIcon
autoFocusboolean
disabledbooleanThe menu will not open; aria-disabled + data-disabled, out of the tab order
PropTypeDefaultDescription
anchorplacement | {to, gap, offset, padding} | false'bottom end'false renders the panel inline
portalbooleanHeadless UI forces the portal on whenever anchor is set
modalboolean
transitionbooleanfalseHeadless UI enter / leave transition (data-closed) instead of the open animation
staticbooleanAlways render, ignoring open state
unmountbooleanKeep the panel mounted (hidden) while closed

MenuItemProps<TTag> extends Headless UI MenuItemProps<TTag> with:

PropTypeDefaultDescription
asElementType'button'Polymorphic; a native button gets type="button"
iconReactNodeBefore the label, aria-hidden
shortcutReactNodeMuted <kbd> at the end, aria-hidden; add aria-keyshortcuts and bind the key yourself
destructivebooleanfalseDestructive text and focus tint; data-destructive
disabledbooleanfalse
childrenReactNode | ({focus, disabled, close}) => ReactNode
classNamestring

Menu.Section, Menu.Heading and Menu.Separator take their element's attributes plus className.

Slots

SlotElementNotes
[data-slot="menu"]divRoot
[data-slot="menu-button"]buttonAlso Button's data-variant / data-size / data-loading and Headless UI data-open / data-active / data-hover / data-focus / data-disabled
[data-slot="menu-chevron"]svg
[data-slot="menu-items"]divThe panel
[data-slot="menu-section"]divrole="group"
[data-slot="menu-heading"]header
[data-slot="menu-separator"]divrole="separator"
[data-slot="menu-item"]button (or as)Also data-destructive, Headless UI data-focus / data-disabled
[data-slot="menu-item-icon"]span
[data-slot="menu-item-content"]spanTruncated label
[data-slot="menu-item-shortcut"]kbd

Tokens

TokenUsed for
Button tokens (--control-height-*, --button-padding-x-*, variant pairs)The trigger
--control-padding-x-smRow inset
--popover, --popover-foreground, --border, --shadow-*Panel surface
--accent, --accent-foregroundFocused row
--destructiveDestructive row
--muted-foregroundHeadings and shortcuts
--duration-fastRow colour transition

Accessibility

  • Headless UI roles: the trigger has aria-haspopup="menu", aria-expanded and aria-controls; the panel is role="menu", rows are role="menuitem", sections role="group" labelled by their heading, separators role="separator".
  • Keyboard: Enter, Space, ArrowDown open and focus the first item (ArrowUp the last); arrows move focus; Home / End jump; typing does typeahead; Enter / Space activate; Escape closes and returns focus to the trigger.
  • Items are native <button type="button">s by default, so a menu inside a form never submits it. A disabled item is skipped by the keyboard.
  • shortcut and icon are aria-hidden. Add aria-keyshortcuts to the item and bind the key yourself.
  • An icon trigger needs an aria-label; otherwise it is announced as "menu button" only.
  • Button: the trigger's variants and sizes.
  • Listbox: for selecting a value rather than running an action.
  • Popover: a floating panel with arbitrary content.
  • Kbd: key caps for shortcut.