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
- Supports items, labels, groups of items
- Focus is fully managed using
aria-activedescendantpattern - Typeahead to allow focusing items by typing text
- Keyboard navigation support including arrow keys, home/end, page up/down
Installation
Install the menu package:
Anatomy
Check the menu anatomy and part names.
Each part includes a
data-partattribute to help identify them in the DOM.
Usage
Import the menu package:
import * as menu from "@zag-js/menu"
The menu package exports two key functions:
machine- Behavior logic for the menu.connect- Maps behavior to JSX props and event handlers.
Pass a unique
idtouseMachineso generated element ids stay predictable.
Then use the framework integration helpers:
- Destructure the service returned from
useMachine. - Use the exposed
setParentandsetChildfunctions provided by the menu's connect function to assign the parent and child menus respectively. - Create trigger items using
api.getTriggerItemProps(...).
When building nested menus, you'll need to use:
setParent(...)- Registers the parent menu on the child menu.setChild(...)- Registers the child menu on the parent menu.
Controlling open state
Use open and onOpenChange to control the open state.
const service = useMachine(menu.machine, { open, onOpenChange(details) { // details => { open: boolean } setOpen(details.open) }, })
Default open state
Use defaultOpen for an uncontrolled initial state.
const service = useMachine(menu.machine, { defaultOpen: true, })
Listening for highlighted changes
Use onHighlightChange to react when keyboard or pointer highlight changes.
const service = useMachine(menu.machine, { onHighlightChange(details) { // details => { highlightedValue: string | null } console.log(details.highlightedValue) }, })
Setting the initial highlighted item
Use defaultHighlightedValue to set the initially highlighted menu item.
const service = useMachine(menu.machine, { defaultHighlightedValue: "copy", })
Listening for item selection
Use onSelect to react when an item is selected.
const service = useMachine(menu.machine, { onSelect(details) { // details => { value: string } console.log(details.value) }, })
Positioning submenus
Use positioning to configure submenu placement.
const service = useMachine(menu.machine, { positioning: { placement: "right-start" }, })
Styling guide
Each menu part includes a data-part attribute you can target in CSS.
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="item"][data-type="radio|checkbox"][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="item"][data-type="radio|checkbox"][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="item"][data-type="radio|checkbox"][data-state="checked"] { /* styles for checked state */ }
Methods and Properties
Machine Context
The menu machine exposes the following context properties:
idsPartial<{ trigger: string | ((value?: string | undefined) => string); contextTrigger: string | ((value?: string | undefined) => string); content: string; groupLabel: (id: string) => string; group: (id: string) => string; positioner: string; arrow: string; }> | undefinedThe ids of the elements in the menu. Useful for composition.defaultHighlightedValuestring | null | undefinedThe initial highlighted value of the menu item when rendered. Use when you don't need to control the highlighted value of the menu item.highlightedValuestring | null | undefinedThe controlled highlighted value of the menu item.onHighlightChange((details: HighlightChangeDetails) => void) | undefinedFunction called when the highlighted menu item changes.onSelect((details: SelectionDetails) => void) | undefinedFunction called when a menu item is selected.anchorPointanyThe positioning point for the menu. Can be set by the context menu trigger or the button trigger.loopFocusboolean | undefinedWhether to loop the keyboard navigation.positioninganyThe options used to dynamically position the menucloseOnSelectboolean | undefinedWhether to close the menu when an option is selectedaria-labelstring | undefinedThe accessibility label for the menuopenboolean | undefinedThe controlled open state of the menuonOpenChange((details: OpenChangeDetails) => void) | undefinedFunction called when the menu opens or closesdefaultOpenboolean | undefinedThe initial open state of the menu when rendered. Use when you don't need to control the open state of the menu.typeaheadboolean | undefinedWhether the pressing printable characters should trigger typeahead navigationcompositeboolean | undefinedWhether the menu is a composed with other composite widgets like a combobox or tabsnavigate((details: NavigateDetails) => void) | null | undefinedFunction to navigate to the selected item if it's an anchor elementtriggerValuestring | null | undefinedThe controlled trigger valuedefaultTriggerValuestring | null | undefinedThe initial trigger value when rendered. Use when you don't need to control the trigger value.onTriggerValueChange((details: TriggerValueChangeDetails) => void) | undefinedFunction called when the trigger value changes.dir"ltr" | "rtl" | undefinedThe document's text/writing direction.idstringThe unique identifier of the machine.getRootNode(() => ShadowRoot | Node | Document) | undefinedA root node to correctly resolve document in custom environments. E.x.: Iframes, Electron.
Machine API
The menu api exposes the following methods:
openbooleanWhether the menu is opensetOpen(open: boolean) => voidFunction to open or close the menutriggerValuestring | nullThe trigger valuesetTriggerValue(value: string | null) => voidFunction to set the trigger valuehighlightedValuestring | nullThe id of the currently highlighted menuitemsetHighlightedValue(value: string) => voidFunction to set the highlighted menuitemsetParent(parent: Service<MenuSchema>) => voidFunction to register a parent menu. This is used for submenussetChild(child: Service<MenuSchema>) => voidFunction to register a child menu. This is used for submenusreposition(options?: any) => voidFunction to reposition the popovergetOptionItemState(props: OptionItemProps) => OptionItemStateReturns the state of the option itemgetItemState(props: ItemProps) => ItemStateReturns the state of the menu itemaddItemListener(props: ItemListenerProps) => VoidFunction | undefinedSetup the custom event listener for item selection event
Data Attributes
CSS Variables
Accessibility
Uses aria-activedescendant pattern to manage focus movement among menu items.
Keyboard Interactions
- SpaceOpens/closes the nested menu.
- EnterOpens/closes the nested menu.
- ArrowDownMoves focus to the next item.
- ArrowUpMoves focus to the previous item.
- ArrowRightOpens the nested menu.
- ArrowLeftCloses the nested menu.
- EscCloses the nested menu and moves focus to the parent menu item.