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- State machine logic.connect- Maps machine state to JSX props and event handlers.
Pass a unique
idtouseMachineso generated element ids stay predictable.
Then use the framework integration helpers:
Listening for item selection
When a menu item is clicked, the onSelect callback is invoked.
const service = useMachine(menu.machine, { onSelect(details) { // details => { value: string } console.log("selected value is ", details.value) }, })
Listening for open state changes
When a menu is opened or closed, the onOpenChange callback is invoked.
const service = useMachine(menu.machine, { onOpenChange(details) { // details => { open: boolean } console.log("open state is ", details.open) }, })
Controlled open state
Use open and onOpenChange to control visibility externally.
const service = useMachine(menu.machine, { open, onOpenChange(details) { setOpen(details.open) }, })
Listening for highlighted items
Use onHighlightChange to react when highlighted item changes.
const service = useMachine(menu.machine, { onHighlightChange(details) { // details => { highlightedValue: string | null } console.log(details.highlightedValue) }, })
Setting initial highlighted item
Use defaultHighlightedValue to set the initially highlighted item.
const service = useMachine(menu.machine, { defaultHighlightedValue: "settings", })
Grouping menu items
When you have many menu items, it can help to group related options:
- Wrap the menu items within an element.
- Spread
api.getGroupProps(...)props on the group element, providing anid. - Render a label for the menu group, providing the
idof the group element.
//... <div {...api.getContentProps()}> {/* ... */} <hr {...api.getSeparatorProps()} /> <p {...api.getItemGroupLabelProps({ htmlFor: "account" })}>Accounts</p> <div {...api.getItemGroupProps({ id: "account" })}> <button {...api.getItemProps({ value: "account-1" })}>Account 1</button> <button {...api.getItemProps({ value: "account-2" })}>Account 2</button> </div> </div> //...
Checkbox and Radio option items
To use checkbox or radio option items, you'll need to:
- Add a
valueproperty to the machine's context whose value is an object describing the state of the option items. - Use the
api.getOptionItemProps(...)function to get the props for the option item.
A common requirement for the option item that you pass the name, value and
type properties.
type— The type of option item. Either"checkbox"or"radio".value— The value of the option item.checked— The checked state of the option item.onCheckedChange— The callback to invoke when the checked state changes.
Default open state
Use defaultOpen to start with the menu opened in uncontrolled mode.
const service = useMachine(menu.machine, { defaultOpen: true, })
Keeping menu open after selection
Set closeOnSelect to false to keep the menu open after selecting an item.
const service = useMachine(menu.machine, { closeOnSelect: false, })
Positioning the menu
Use positioning to configure menu placement.
const service = useMachine(menu.machine, { positioning: { placement: "bottom-start" }, })
Labeling the menu without visible text
If you do not render a visible label, provide aria-label.
const service = useMachine(menu.machine, { "aria-label": "Actions", })
Styling guide
Each menu part includes a data-part attribute you can target in CSS.
Open and closed state
When the menu is open or closed, the content and trigger parts will have the
data-state attribute.
[data-part="content"][data-state="open|closed"] { /* styles for open or closed state */ } [data-part="trigger"][data-state="open|closed"] { /* styles for open or closed state */ }
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; contextTrigger: string; content: string; groupLabel: (id: string) => string; group: (id: string) => string; positioner: string; arrow: string; }>The ids of the elements in the menu. Useful for composition.defaultHighlightedValuestringThe initial highlighted value of the menu item when rendered. Use when you don't need to control the highlighted value of the menu item.highlightedValuestringThe controlled highlighted value of the menu item.onHighlightChange(details: HighlightChangeDetails) => voidFunction called when the highlighted menu item changes.onSelect(details: SelectionDetails) => voidFunction called when a menu item is selected.anchorPointPointThe positioning point for the menu. Can be set by the context menu trigger or the button trigger.loopFocusbooleanWhether to loop the keyboard navigation.positioningPositioningOptionsThe options used to dynamically position the menucloseOnSelectbooleanWhether to close the menu when an option is selectedaria-labelstringThe accessibility label for the menuopenbooleanThe controlled open state of the menuonOpenChange(details: OpenChangeDetails) => voidFunction called when the menu opens or closesdefaultOpenbooleanThe initial open state of the menu when rendered. Use when you don't need to control the open state of the menu.typeaheadbooleanWhether the pressing printable characters should trigger typeahead navigationcompositebooleanWhether the menu is a composed with other composite widgets like a combobox or tabsnavigate(details: NavigateDetails) => voidFunction to navigate to the selected item if it's an anchor elementdir"ltr" | "rtl"The document's text/writing direction.idstringThe unique identifier of the machine.getRootNode() => ShadowRoot | Node | DocumentA root node to correctly resolve document in custom environments. E.x.: Iframes, Electron.onEscapeKeyDown(event: KeyboardEvent) => voidFunction called when the escape key is pressedonRequestDismiss(event: LayerDismissEvent) => voidFunction called when this layer is closed due to a parent layer being closedonPointerDownOutside(event: PointerDownOutsideEvent) => voidFunction called when the pointer is pressed down outside the componentonFocusOutside(event: FocusOutsideEvent) => voidFunction called when the focus is moved outside the componentonInteractOutside(event: InteractOutsideEvent) => voidFunction called when an interaction happens outside the component
Machine API
The menu api exposes the following methods:
openbooleanWhether the menu is opensetOpen(open: boolean) => voidFunction to open or close the menuhighlightedValuestringThe id of the currently highlighted menuitemsetHighlightedValue(value: string) => voidFunction to set the highlighted menuitemsetParent(parent: ParentMenuService) => voidFunction to register a parent menu. This is used for submenussetChild(child: ChildMenuService) => voidFunction to register a child menu. This is used for submenusreposition(options?: Partial<PositioningOptions>) => 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) => VoidFunctionSetup 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
- 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