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>.
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.
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.
Links and placement
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.
Props
MenuProps extends div attributes with:
| Prop | Type | Description |
|---|---|---|
children | ReactNode | ({open, close}) => ReactElement | Menu.Button + Menu.Items, or a render function |
className | string | Merged last onto the root |
Menu.Button
Every Button prop except as, plus:
| Prop | Type | Default | Description |
|---|---|---|---|
variant | ButtonVariant | 'outline' | |
size | 'sm' | 'md' | 'lg' | 'icon' | 'md' | icon needs an aria-label |
hideChevron | boolean | false | Never drawn on an icon button or beside a custom trailingIcon |
autoFocus | boolean | — | |
disabled | boolean | — | The menu will not open; aria-disabled + data-disabled, out of the tab order |
Menu.Items
| Prop | Type | Default | Description |
|---|---|---|---|
anchor | placement | {to, gap, offset, padding} | false | 'bottom end' | false renders the panel inline |
portal | boolean | — | Headless UI forces the portal on whenever anchor is set |
modal | boolean | — | |
transition | boolean | false | Headless UI enter / leave transition (data-closed) instead of the open animation |
static | boolean | — | Always render, ignoring open state |
unmount | boolean | — | Keep the panel mounted (hidden) while closed |
Menu.Item
MenuItemProps<TTag> extends Headless UI MenuItemProps<TTag> with:
| Prop | Type | Default | Description |
|---|---|---|---|
as | ElementType | 'button' | Polymorphic; a native button gets type="button" |
icon | ReactNode | — | Before the label, aria-hidden |
shortcut | ReactNode | — | Muted <kbd> at the end, aria-hidden; add aria-keyshortcuts and bind the key yourself |
destructive | boolean | false | Destructive text and focus tint; data-destructive |
disabled | boolean | false | |
children | ReactNode | ({focus, disabled, close}) => ReactNode | — | |
className | string | — |
Menu.Section, Menu.Heading and Menu.Separator take their element's
attributes plus className.
Slots
| Slot | Element | Notes |
|---|---|---|
[data-slot="menu"] | div | Root |
[data-slot="menu-button"] | button | Also 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"] | div | The panel |
[data-slot="menu-section"] | div | role="group" |
[data-slot="menu-heading"] | header | — |
[data-slot="menu-separator"] | div | role="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"] | span | Truncated label |
[data-slot="menu-item-shortcut"] | kbd | — |
Tokens
| Token | Used for |
|---|---|
Button tokens (--control-height-*, --button-padding-x-*, variant pairs) | The trigger |
--control-padding-x-sm | Row inset |
--popover, --popover-foreground, --border, --shadow-* | Panel surface |
--accent, --accent-foreground | Focused row |
--destructive | Destructive row |
--muted-foreground | Headings and shortcuts |
--duration-fast | Row colour transition |
Accessibility
- Headless UI roles: the trigger has
aria-haspopup="menu",aria-expandedandaria-controls; the panel isrole="menu", rows arerole="menuitem", sectionsrole="group"labelled by their heading, separatorsrole="separator". - Keyboard:
Enter,Space,ArrowDownopen and focus the first item (ArrowUpthe last); arrows move focus;Home/Endjump; typing does typeahead;Enter/Spaceactivate;Escapecloses and returns focus to the trigger. - Items are native
<button type="button">s by default, so a menu inside a form never submits it. Adisableditem is skipped by the keyboard. shortcutandiconarearia-hidden. Addaria-keyshortcutsto the item and bind the key yourself.- An
icontrigger needs anaria-label; otherwise it is announced as "menu button" only.