Nested Menu
An accessible dropdown and context menu that is used to display a list of actions or options that a user can choose.
Features
- Support for items, labels, groups of items.
- Focus is fully managed using
aria-activedescendant
pattern. - Typeahead to allow focusing items by typing text.
- Keyboard navigation support including arrow keys, home/end, page up/down.
Installation
To use the menu machine in your project, run the following command in your command line:
This command will install the framework agnostic menu logic and the reactive utilities for your framework of choice.
Anatomy
To set up the menu correctly, you'll need to understand its anatomy and how we name its parts.
Each part includes a
data-part
attribute to help identify them in the DOM.
Usage
First, import the menu package into your project
import * as menu from "@zag-js/menu"
The menu package exports two key functions:
machine
โ The state machine logic for the menu widget.connect
โ The function that translates the machine's state to JSX attributes and event handlers.
You'll need to provide a unique
id
to theuseMachine
hook. This is used to ensure that every part has a unique identifier.
Next, import the required hooks and functions for your framework and use the menu machine in your project ๐ฅ
- Destructure the machine's service returned from the
useMachine
hook. - Use the exposed
setParent
andsetChild
functions provided by the menu's connect function to assign the parent and child menus respectively. - Create trigger item's using the
api.getTriggerItemProps(...)
function.
When building nested menus, you'll need to use:
setParent(...)
โ Function to register a parent menu's machine in the child menu's context.setChild(...)
โ Function to register a child menu's machine in the parent menu's context.
Styling guide
Earlier, we mentioned that each menu part has a data-part
attribute added to
them to select and style them in the DOM.
Highlighted item state
When an item is highlighted, via keyboard navigation or pointer, it is given a
data-highlighted
attribute.
[data-part="item"][data-highlighted] { /* styles for highlighted state */ } [data-part="option-item"][data-highlighted] { /* styles for highlighted state */ }
Disabled item state
When an item or an option item is disabled, it is given a data-disabled
attribute.
[data-part="item"][data-disabled] { /* styles for disabled state */ } [data-part="option-item"][data-disabled] { /* styles for disabled state */ }
Using arrows
When using arrows within the menu, you can style it using css variables.
[data-part="arrow"] { --arrow-size: 20px; --arrow-background: red; }
Checked option item state
When an option item is checked, it is given a data-state
attribute.
[data-part="option-item"][data-state="checked"] { /* styles for checked state */ }
Methods and Properties
Machine Context
The menu machine exposes the following context properties:
ids
Partial<{ trigger: string; contextTrigger: string; content: string; label(id: string): string; group(id: string): string; positioner: string; arrow: string; }>
The ids of the elements in the menu. Useful for composition.value
Record<string, string | string[]>
The values of radios and checkboxes in the menu.onValueChange
(details: ValueChangeDetails) => void
Callback to be called when the menu values change (for radios and checkboxes).highlightedId
string
The `id` of the active menu item.onSelect
(details: SelectionDetails) => void
Function called when a menu item is selected.anchorPoint
Point
The positioning point for the menu. Can be set by the context menu trigger or the button trigger.loop
boolean
Whether to loop the keyboard navigation.positioning
PositioningOptions
The options used to dynamically position the menucloseOnSelect
boolean
Whether to close the menu when an option is selectedaria-label
string
The accessibility label for the menuopen
boolean
Whether the menu is openonOpenChange
(details: OpenChangeDetails) => void
Function called when the menu opens or closesopen.controlled
boolean
Whether the menu's open state is controlled by the userdir
"ltr" | "rtl"
The document's text/writing direction.id
string
The unique identifier of the machine.getRootNode
() => ShadowRoot | Node | Document
A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron.onPointerDownOutside
(event: PointerDownOutsideEvent) => void
Function called when the pointer is pressed down outside the componentonFocusOutside
(event: FocusOutsideEvent) => void
Function called when the focus is moved outside the componentonInteractOutside
(event: InteractOutsideEvent) => void
Function called when an interaction happens outside the component
Machine API
The menu api
exposes the following methods:
isOpen
boolean
Whether the menu is openopen
() => void
Function to open the menuclose
() => void
Function to close the menuhighlightedId
string
The id of the currently highlighted menuitemsetHighlightedId
(id: string) => void
Function to set the highlighted menuitemsetParent
(parent: Service) => void
Function to register a parent menu. This is used for submenussetChild
(child: Service) => void
Function to register a child menu. This is used for submenusvalue
Record<string, string | string[]>
The value of the menu options itemsetValue
(name: string, value: any) => void
Function to set the value of the menu options itemreposition
(options?: Partial<PositioningOptions>) => void
Function to reposition the popovergetOptionItemState
(props: OptionItemProps) => OptionItemState
Returns the state of the option itemgetItemState
(props: ItemProps) => ItemState
Returns the state of the menu item
Accessibility
Uses aria-activedescendant pattern to manage focus movement among menu items.
Keyboard Interactions
- SpaceActivates/Selects the highlighted item
- EnterActivates/Selects the highlighted item
- ArrowDownHighlights the next item in the menu
- ArrowUpHighlights the previous item in the menu
- ArrowRightArrowLeftWhen focus is on trigger, opens or closes the submenu depending on reading direction.
- EscCloses the menu and moves focus to the trigger
Edit this page on GitHub