Skip to main content
0.27.1
View Zag.js on Github
Join the Discord server

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:

npm install @zag-js/menu @zag-js/react # or yarn add @zag-js/menu @zag-js/react

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 the useMachine 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 🔥

import * as menu from "@zag-js/menu" import { useMachine, normalizeProps } from "@zag-js/react" export function Menu() { const [state, send] = useMachine(menu.machine({ id: "1", "aria-label": "File" })) const api = menu.connect(state, send, normalizeProps) return ( <div> <button {...api.triggerProps}> Actions <span aria-hidden></span> </button> <div {...api.positionerProps}> <ul {...api.contentProps}> <li {...api.getItemProps({ id: "edit" })}>Edit</li> <li {...api.getItemProps({ id: "duplicate" })}>Duplicate</li> <li {...api.getItemProps({ id: "delete" })}>Delete</li> <li {...api.getItemProps({ id: "export" })}>Export...</li> </ul> </div> </div> ) }

Listening for item selection

When a menu item is clicked, the onSelect callback is invoked.

const [state, send] = 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 [state, send] = useMachine( menu.machine({ onOpenChange(details) { // details => { open: boolean } console.log("open state is ", details.open) }, }), )

Grouping menu items

When the number of menu items gets much, it might be useful to group related menu items. To achieve this:

  • Wrap the menu items within an element.
  • Spread the api.groupProps(...) JSX properties unto the element, providing an id.
  • Render a label for the menu group, providing the id of the group element.
//... <div {...api.contentProps}> {/* ... */} <hr {...api.separatorProps} /> <p {...api.getLabelProps({ htmlFor: "account" })}>Accounts</p> <div {...api.getGroupProps({ id: "account" })}> <button {...api.getItemProps({ id: "account-1" })}>Account 1</button> <button {...api.getItemProps({ id: "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 value property 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.

  • name — The property key in the value context that this option is for.
  • type — The type of option item. Either "checkbox" or "radio".
  • value — The value of the option item.
import * as menu from "@zag-js/menu" import { useMachine, normalizeProps } from "@zag-js/react" const data = { order: [ { label: "Ascending", id: "asc" }, { label: "Descending", id: "desc" }, { label: "None", id: "none" }, ], type: [ { label: "Email", id: "email" }, { label: "Phone", id: "phone" }, { label: "Address", id: "address" }, ], } export function Menu() { const [state, send] = useMachine( menu.machine({ id: "1", "aria-label": "Sort by", value: { order: "", type: [] }, }), ) const api = menu.connect(state, send, normalizeProps) return ( <> <button {...api.triggerProps}>Trigger</button> <div {...api.positionerProps}> <div {...api.contentProps}> {data.order.map((item) => { const option = { type: "radio", name: "order", value: item.id } return ( <div key={item.id} {...api.getOptionItemProps(option)}> <span {...api.getOptionItemIndicatorProps(opts)}></span> <span {...api.getOptionItemTextProps(opts)}>{item.label}</span> </div> ) })} <hr {...api.separatorProps} /> {data.type.map((item) => { const option = { type: "checkbox", name: "type", value: item.id } return ( <div key={item.id} {...api.getOptionItemProps(option)}> <span {...api.getOptionItemIndicatorProps(opts)}></span> <span {...api.getOptionItemTextProps(opts)}>{item.label}</span> </div> ) })} </div> </div> </> ) }

The machine invokes an onValueChange callback when an radio or checkbox option checked state changes. This callback is invoked with the name of the option and its new value.

const [state, send] = useMachine( menu.machine({ value: { order: "", type: [] }, onValueChange(data) { // data => { name: string, value: string | string[] } console.log("values changed", data.value) }, }), )

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.

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="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:

  • idsPartial<{ 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.
  • valueRecord<string, string | string[]>The values of radios and checkboxes in the menu.
  • onValueChange(details: ValueChangeDetails) => voidCallback to be called when the menu values change (for radios and checkboxes).
  • highlightedIdstringThe `id` of the active menu item.
  • 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.
  • loopbooleanWhether to loop the keyboard navigation.
  • positioningPositioningOptionsThe options used to dynamically position the menu
  • closeOnSelectbooleanWhether to close the menu when an option is selected
  • aria-labelstringThe accessibility label for the menu
  • onOpenChange(details: OpenChangeDetails) => voidFunction called when the menu opens or closes
  • dir"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.
  • onPointerDownOutside(event: PointerDownOutsideEvent) => voidFunction called when the pointer is pressed down outside the combobox
  • onFocusOutside(event: FocusOutsideEvent) => voidFunction called when the focus is moved outside the combobox
  • onInteractOutside(event: InteractOutsideEvent) => voidFunction called when an interaction happens outside the combobox

Machine API

The menu api exposes the following methods:

  • isOpenbooleanWhether the menu is open
  • open() => voidFunction to open the menu
  • close() => voidFunction to close the menu
  • highlightedIdstringThe id of the currently highlighted menuitem
  • setHighlightedId(id: string) => voidFunction to set the highlighted menuitem
  • setParent(parent: Service) => voidFunction to register a parent menu. This is used for submenus
  • setChild(child: Service) => voidFunction to register a child menu. This is used for submenus
  • valueRecord<string, string | string[]>The value of the menu options item
  • setValue(name: string, value: any) => voidFunction to set the value of the menu options item
  • reposition(options?: Partial<PositioningOptions>) => voidFunction to reposition the popover
  • getOptionItemState(props: OptionItemProps) => OptionItemStateReturns the state of the option item
  • getItemState(props: ItemProps) => ItemStateReturns the state of the menu item

Edit this page on GitHub

On this page