Menu
View as MarkdownAn accessible dropdown and context menu that displays a list of actions or options.
<ui:menu.root>
<ui:menu.trigger>Open Menu</ui:menu.trigger>
<ui:menu.content>
<ui:menu.item value="new-file">New File</ui:menu.item>
<ui:menu.item value="new-window">New Window</ui:menu.item>
<ui:menu.separator />
<ui:menu.item value="settings">Settings</ui:menu.item>
</ui:menu.content>
</ui:menu.root>
import { mountAll } from 'fluid-primitives';
import { Menu } from 'fluid-primitives/menu';
mountAll('ui:menu', ({ props }) => {
const menu = new Menu(props);
menu.init();
return menu;
});
Features
Permalink to heading "Features"- Support for items, labels, and groups of items
- Support for checkbox and radio menu items
- Support for nested/submenu patterns
- Support for context menus (triggered by right-click) and multiple independent triggers sharing one menu
- Focus is fully managed using the
aria-activedescendantpattern - Typeahead to allow focusing items by typing text
- Full keyboard navigation support, including arrow keys, home/end, and submenu navigation
Installation
Permalink to heading "Installation"typo3 ui:add menu
Please copy the files manually from GitHub into your project.
Read more about installing Components and Primitives.
Examples
Permalink to heading "Examples"Grouping
Permalink to heading "Grouping"Organize items into logical groups with their own labels.
<ui:menu.root>
<ui:menu.trigger>File</ui:menu.trigger>
<ui:menu.content>
<ui:menu.itemGroup value="file-group">
<ui:menu.itemGroupLabel>File</ui:menu.itemGroupLabel>
<ui:menu.item value="new">New</ui:menu.item>
<ui:menu.item value="open">Open</ui:menu.item>
<ui:menu.item value="save">Save</ui:menu.item>
</ui:menu.itemGroup>
<ui:menu.separator />
<ui:menu.itemGroup value="edit-group">
<ui:menu.itemGroupLabel>Edit</ui:menu.itemGroupLabel>
<ui:menu.item value="undo">Undo</ui:menu.item>
<ui:menu.item value="redo">Redo</ui:menu.item>
</ui:menu.itemGroup>
</ui:menu.content>
</ui:menu.root>
With Links
Permalink to heading "With Links"Pass asChild="{true}" on menu.item to render the item's attributes and behavior directly onto a
single child element, like a plain <a href>, so it behaves like real navigation, including
keyboard activation.
<ui:menu.root>
<ui:menu.trigger>Navigate</ui:menu.trigger>
<ui:menu.content>
<ui:menu.item asChild="{true}" value="home">
<a href="/">Home</a>
</ui:menu.item>
<ui:menu.item asChild="{true}" value="docs">
<a href="/docs/introduction">Documentation</a>
</ui:menu.item>
<ui:menu.item asChild="{true}" value="github">
<a href="https://github.com/jramke/fluid-primitives" target="_blank" rel="noreferrer">GitHub</a>
</ui:menu.item>
</ui:menu.content>
</ui:menu.root>
With Checkboxes
Permalink to heading "With Checkboxes"Use menu.checkboxItem for items that toggle independently of each other.
<ui:menu.root closeOnSelect="{false}">
<ui:menu.trigger>View</ui:menu.trigger>
<ui:menu.content>
<ui:menu.checkboxItem value="toolbar">Show Toolbar</ui:menu.checkboxItem>
<ui:menu.checkboxItem value="sidebar" checked="{true}">Show Sidebar</ui:menu.checkboxItem>
<ui:menu.checkboxItem value="statusbar">Show Status Bar</ui:menu.checkboxItem>
</ui:menu.content>
</ui:menu.root>
With Radios
Permalink to heading "With Radios"Use menu.radioItem with a shared name for mutually exclusive options.
<ui:menu.root closeOnSelect="{false}">
<ui:menu.trigger>Sort By</ui:menu.trigger>
<ui:menu.content>
<ui:menu.radioItem name="sort" value="name" checked="{true}">Name</ui:menu.radioItem>
<ui:menu.radioItem name="sort" value="date">Date Modified</ui:menu.radioItem>
<ui:menu.radioItem name="sort" value="size">Size</ui:menu.radioItem>
</ui:menu.content>
</ui:menu.root>
Nested Menu
Permalink to heading "Nested Menu"A submenu is just another menu.root, linked to its parent by two explicit ids: give the submenu's
menu.root a rootId and point its parentId back at the parent menu's own rootId, then render a
menu.triggerItem inside the parent's own content with a childId matching the submenu's rootId.
The submenu itself doesn't need to live anywhere near the parent's markup - it's linked entirely by id.
<ui:menu.root rootId="file-menu">
<ui:menu.trigger>File</ui:menu.trigger>
<ui:menu.content>
<ui:menu.item value="new-tab">New Tab</ui:menu.item>
<ui:menu.item value="new-window">New Window</ui:menu.item>
<ui:menu.separator />
<ui:menu.triggerItem childId="share-menu">Share</ui:menu.triggerItem>
<ui:menu.triggerItem childId="export-menu">Export</ui:menu.triggerItem>
<ui:menu.separator />
<ui:menu.item value="print">Print…</ui:menu.item>
</ui:menu.content>
</ui:menu.root>
<ui:menu.root rootId="share-menu" parentId="file-menu" positioning="{ placement: 'right-start' }">
<ui:menu.content>
<ui:menu.item value="email">Email</ui:menu.item>
<ui:menu.item value="messages">Messages</ui:menu.item>
<ui:menu.item value="airdrop">AirDrop</ui:menu.item>
</ui:menu.content>
</ui:menu.root>
<ui:menu.root rootId="export-menu" parentId="file-menu" positioning="{ placement: 'right-start' }">
<ui:menu.content>
<ui:menu.item value="pdf">PDF</ui:menu.item>
<ui:menu.item value="png">PNG</ui:menu.item>
<ui:menu.item value="svg">SVG</ui:menu.item>
</ui:menu.content>
</ui:menu.root>
Context Menu
Permalink to heading "Context Menu"Use menu.contextTrigger to open the menu on right-click instead of (or in addition to) a regular trigger.
<ui:menu.root>
<ui:menu.contextTrigger>
Right-click this area
</ui:menu.contextTrigger>
<ui:menu.content>
<ui:menu.item value="cut">Cut</ui:menu.item>
<ui:menu.item value="copy">Copy</ui:menu.item>
<ui:menu.item value="paste">Paste</ui:menu.item>
</ui:menu.content>
</ui:menu.root>
Multiple Triggers
Permalink to heading "Multiple Triggers"Give several menu.trigger elements different values to share a single menu instance between them.
<ui:menu.root>
<div class="flex gap-2">
<ui:menu.trigger value="a">Trigger A</ui:menu.trigger>
<ui:menu.trigger value="b">Trigger B</ui:menu.trigger>
</div>
<ui:menu.content>
<ui:menu.item value="edit">Edit</ui:menu.item>
<ui:menu.item value="duplicate">Duplicate</ui:menu.item>
<ui:menu.item value="delete">Delete</ui:menu.item>
</ui:menu.content>
</ui:menu.root>
Inside a Dialog
Permalink to heading "Inside a Dialog"By default menu.content portals to the end of the document body, same as Select. Pass
portalled="{false}" when nesting a menu inside another portalled/focus-trapped element, like a
dialog, so it stays within that element's DOM subtree instead.
<ui:dialog.root>
<ui:dialog.trigger asChild="{true}">
<ui:button>Open Dialog</ui:button>
</ui:dialog.trigger>
<ui:dialog.content>
<ui:dialog.header>
<ui:dialog.title>Project Settings</ui:dialog.title>
<ui:dialog.description>
The menu below sets <code>portalled="{false}"</code> on <code>ui:menu.content</code> so it
stays inside this dialog's DOM subtree instead of portalling to the document body.
</ui:dialog.description>
</ui:dialog.header>
<ui:menu.root>
<ui:menu.trigger class="self-start">Options</ui:menu.trigger>
<ui:menu.content portalled="{false}">
<ui:menu.item value="rename">Rename</ui:menu.item>
<ui:menu.item value="duplicate">Duplicate</ui:menu.item>
<ui:menu.item value="archive">Archive</ui:menu.item>
</ui:menu.content>
</ui:menu.root>
<ui:dialog.footer>
<ui:dialog.close asChild="{true}">
<ui:button variant="secondary">Close</ui:button>
</ui:dialog.close>
</ui:dialog.footer>
</ui:dialog.content>
</ui:dialog.root>
API Reference
Permalink to heading "API Reference"The following tables cover the available props of the Fluid Primitives. For a full list of available client side props and methods, see the Zag.js Machine API.
menu.root
Provides shared menu state to the rest of the parts. Renders no element of its own.
| Name | Description | Required | Default |
|---|---|---|---|
closeOnSelect | booleanWhether to close the menu when an option is selected. | No | true |
composite | booleanWhether the menu is composed with other composite widgets like a combobox or tabs. | No | true |
typeahead | booleanWhether pressing printable characters should trigger typeahead navigation. | No | true |
loopFocus | booleanWhether to loop the keyboard navigation. | No | false |
positioning | arrayThe options used to dynamically position the menu. | No | {
"placement": "bottom-start",
"gutter": 8
} |
defaultOpen | booleanThe initial open state of the menu when rendered. Use when you don't need to control the open state of the menu. | No | false |
defaultHighlightedValue | stringThe initial highlighted value of the menu item when rendered. | No | - |
defaultTriggerValue | stringThe initial trigger value when rendered, for a menu with multiple triggers. | No | - |
ariaLabel | stringThe accessibility label for the menu. Forwarded to the machine as `aria-label`. | No | - |
parentId | stringThe `rootId` of the parent menu this menu is a submenu of. Must match a `menu.triggerItem`'s `childId` on that parent, pointing back at this menu's own `rootId` - see the Nested Menu docs example. | No | - |
rootId | stringThe root ID of the component, used for hydration and identification. | No | - |
ids | arrayThe IDs of of the component parts for composition. | No | [] |
controlled | booleanIf true, the component is meant to be initialized manually inside another component | No | false |
menu.trigger
Opens and closes the menu. Renders a <button> element.
| Name | Description | Required | Default |
|---|---|---|---|
value | stringThe value that identifies this specific trigger, for a menu with multiple triggers. | No | - |
asChild | booleanIf true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. | No | - |
class | stringThe CSS class(es) to be applied to the component. | No | - |
attributes | arrayAdditional attributes that should be rendered on the component where ui:attributes is used. | No | [] |
Rendered data attributes
| Attribute | Description |
|---|---|
data-scope | menu |
data-part | trigger |
data-placement | The placement of the trigger |
data-side | The side of the trigger that the trigger is positioned on |
data-value | The value of the item |
data-current | Present when current |
data-controls | |
data-state | "open" | "closed" |
menu.contextTrigger
Opens the menu at the cursor position on right-click. Renders a <div> element.
| Name | Description | Required | Default |
|---|---|---|---|
value | stringThe value that identifies this specific context trigger, for a menu with multiple context triggers. | No | - |
asChild | booleanIf true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. | No | - |
class | stringThe CSS class(es) to be applied to the component. | No | - |
attributes | arrayAdditional attributes that should be rendered on the component where ui:attributes is used. | No | [] |
Rendered data attributes
| Attribute | Description |
|---|---|
data-scope | menu |
data-part | context-trigger |
data-value | The value of the item |
data-current | Present when current |
data-state | "open" | "closed" |
menu.content
Contains the menu items. Renders a <div> element.
| Name | Description | Required | Default |
|---|---|---|---|
asChild | booleanIf true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. | No | - |
class | stringThe CSS class(es) to be applied to the component. | No | - |
attributes | arrayAdditional attributes that should be rendered on the component where ui:attributes is used. | No | [] |
Rendered data attributes
| Attribute | Description |
|---|---|
data-scope | menu |
data-part | content |
data-state | "open" | "closed" |
data-nested | menu |
data-has-nested | menu |
data-placement | The placement of the content |
data-side | The side of the trigger that the content is positioned on |
menu.item
A selectable menu action. Renders a <div> element.
| Name | Description | Required | Default |
|---|---|---|---|
value | stringThe unique value of the menu item. | Yes | - |
disabled | booleanWhether the menu item is disabled. | No | - |
valueText | stringThe textual value of the item, used for typeahead navigation. Falls back to the item's text content when omitted. | No | - |
asChild | booleanIf true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. | No | - |
class | stringThe CSS class(es) to be applied to the component. | No | - |
attributes | arrayAdditional attributes that should be rendered on the component where ui:attributes is used. | No | [] |
Rendered data attributes
| Attribute | Description |
|---|---|
data-scope | menu |
data-part | item |
data-disabled | Present when disabled |
data-highlighted | Present when highlighted |
data-value | The value of the item |
data-valuetext | The human-readable value |
menu.checkboxItem
A menu item that toggles independently of other items. Renders a <div> element.
| Name | Description | Required | Default |
|---|---|---|---|
value | stringThe unique value of the checkbox item. | Yes | - |
checked | booleanWhether the checkbox item is checked when rendered. | No | false |
disabled | booleanWhether the checkbox item is disabled. | No | - |
valueText | stringThe textual value of the item, used for typeahead navigation. Falls back to the item's text content when omitted. | No | - |
asChild | booleanIf true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. | No | - |
class | stringThe CSS class(es) to be applied to the component. | No | - |
attributes | arrayAdditional attributes that should be rendered on the component where ui:attributes is used. | No | [] |
menu.radioItem
A menu item that's mutually exclusive with other radio items sharing the same name. Renders a <div> element.
| Name | Description | Required | Default |
|---|---|---|---|
value | stringThe unique value of the radio item. | Yes | - |
checked | booleanWhether the radio item is checked when rendered. | No | false |
disabled | booleanWhether the radio item is disabled. | No | - |
valueText | stringThe textual value of the item, used for typeahead navigation. Falls back to the item's text content when omitted. | No | - |
name | stringGroups radio items so selecting one unchecks the others sharing the same `name`. | No | - |
asChild | booleanIf true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. | No | - |
class | stringThe CSS class(es) to be applied to the component. | No | - |
attributes | arrayAdditional attributes that should be rendered on the component where ui:attributes is used. | No | [] |
menu.itemGroup
Groups related items together. Renders a <div> element.
| Name | Description | Required | Default |
|---|---|---|---|
value | stringThe value identifier of the item group. Picked up automatically by a `menu.itemGroupLabel` nested inside it. | Yes | - |
asChild | booleanIf true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. | No | - |
class | stringThe CSS class(es) to be applied to the component. | No | - |
attributes | arrayAdditional attributes that should be rendered on the component where ui:attributes is used. | No | [] |
menu.itemGroupLabel
Labels an itemGroup. Renders a <div> element.
| Name | Description | Required | Default |
|---|---|---|---|
asChild | booleanIf true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. | No | - |
class | stringThe CSS class(es) to be applied to the component. | No | - |
attributes | arrayAdditional attributes that should be rendered on the component where ui:attributes is used. | No | [] |
menu.separator
A visual divider between items. Renders a <div> element.
| Name | Description | Required | Default |
|---|---|---|---|
asChild | booleanIf true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. | No | - |
class | stringThe CSS class(es) to be applied to the component. | No | - |
attributes | arrayAdditional attributes that should be rendered on the component where ui:attributes is used. | No | [] |
menu.triggerItem
Opens a nested submenu from within another menu's content. Renders a <div> element.
| Name | Description | Required | Default |
|---|---|---|---|
childId | stringThe `rootId` of the submenu (its `menu.root`'s `rootId`) that this item opens. That submenu's own `parentId` must point back at this menu's `rootId`. | Yes | - |
asChild | booleanIf true the component uses its child only without the component template. Like Radix UI asChild or Base UI render props. | No | - |
class | stringThe CSS class(es) to be applied to the component. | No | - |
attributes | arrayAdditional attributes that should be rendered on the component where ui:attributes is used. | No | [] |
Machine JavaScript API
| Name | Type | Description |
|---|---|---|
open | boolean | Whether the menu is open |
setOpen | (open: boolean) => void | Function to open or close the menu |
triggerValue | string | null | The trigger value |
setTriggerValue | (value: string | null) => void | Function to set the trigger value |
highlightedValue | string | null | The id of the currently highlighted menuitem |
setHighlightedValue | (value: string) => void | Function to set the highlighted menuitem |
setParent | (parent: MenuService) => void | Function to register a parent menu. This is used for submenus |
setChild | (child: MenuService) => void | Function to register a child menu. This is used for submenus |
reposition | (options?: Partial<PositioningOptions>) => void | Function to reposition the popover |
getOptionItemState | (props: OptionItemProps) => OptionItemState | Returns the state of the option item |
getItemState | (props: ItemProps) => ItemState | Returns the state of the menu item |
addItemListener | (props: ItemListenerProps) => VoidFunction | undefined | Setup the custom event listener for item selection event |
Accessibility
| Key | Description |
|---|---|
Space | Activates/Selects the highlighted item |
Enter | Activates/Selects the highlighted item |
ArrowDown | Highlights the next item in the menu |
ArrowUp | Highlights the previous item in the menu |
ArrowRightArrowLeft | When focus is on trigger, opens or closes the submenu depending on reading direction. |
Esc | Closes the menu and moves focus to the trigger |
<primitives:menu.root>
<primitives:menu.trigger>
<primitives:menu.indicator />
</primitives:menu.trigger>
<primitives:menu.contextTrigger />
<primitives:menu.positioner>
<primitives:menu.arrow />
<primitives:menu.content>
<primitives:menu.item>
<primitives:menu.itemText />
</primitives:menu.item>
<primitives:menu.checkboxItem>
<primitives:menu.itemIndicator />
<primitives:menu.itemText />
</primitives:menu.checkboxItem>
<primitives:menu.radioItem>
<primitives:menu.itemIndicator />
<primitives:menu.itemText />
</primitives:menu.radioItem>
<primitives:menu.separator />
<primitives:menu.itemGroup>
<primitives:menu.itemGroupLabel />
</primitives:menu.itemGroup>
<f:comment><!-- Opens the submenu below, matched by rootId/childId --></f:comment>
<primitives:menu.triggerItem childId="submenu" />
</primitives:menu.content>
</primitives:menu.positioner>
</primitives:menu.root>
<f:comment><!-- A submenu: linked to its parent by id, not by nesting --></f:comment>
<primitives:menu.root rootId="submenu" parentId="parent-rootId">
<primitives:menu.positioner>
<primitives:menu.content>
<primitives:menu.item />
</primitives:menu.content>
</primitives:menu.positioner>
</primitives:menu.root>