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

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:

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.

On a high level, the menu consists of:

  • Trigger: The element that toggles the menu.
  • Positioner: The element that positions the menu dynamically.
  • Content: The element that contains the menu items and groups.
  • Item: The menu item used to trigger an action.

The optional parts include:

  • Option Item: The menu item that acts as a radio or checkbox.
  • Context Trigger: The trigger for the menu item.
  • Separator: The element that is used to visually separate menu items.

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 useSetup 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 and setChild 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.
import { Portal } from "@reach/portal" import * as menu from "@zag-js/menu" import { useMachine, normalizeProps } from "@zag-js/react" import { useEffect } from "react" export function NestedMenu() { // Level 1 - File Menu const [fileMenuState, fileMenuSend, fileMenuMachine] = useMachine( menu.machine({ id: "1", "aria-label": "File" }), ) const fileMenu = menu.connect(fileMenuState, fileMenuSend, normalizeProps) // Level 2 - Share Menu const [shareMenuState, shareMenuSend, shareMenuMachine] = useMachine( menu.machine({ id: "2", "aria-label": "Share" }), ) const shareMenu = menu.connect(shareMenuState, shareMenuSend, normalizeProps) useEffect(() => { setTimeout(() => { fileMenu.setChild(shareMenuMachine) shareMenu.setParent(fileMenuMachine) }) }, []) // Share menu trigger const shareMenuTriggerProps = fileMenu.getTriggerItemProps(shareMenu) return ( <> <button {...fileMenu.triggerProps}>Click me</button> <Portal> <div {...fileMenu.positionerProps}> <ul {...fileMenu.contentProps}> <li {...fileMenu.getItemProps({ id: "new-tab" })}>New tab</li> <li {...fileMenu.getItemProps({ id: "new-win" })}>New window</li> <li {...shareMenuTriggerProps}>Share</li> <li {...fileMenu.getItemProps({ id: "print" })}>Print...</li> <li {...fileMenu.getItemProps({ id: "help" })}>Help</li> </ul> </div> </Portal> <Portal> <div {...shareMenu.positionerProps}> <ul {...shareMenu.contentProps}> <li {...shareMenu.getItemProps({ id: "messages" })}>Messages</li> <li {...shareMenu.getItemProps({ id: "airdrop" })}>Airdrop</li> <li {...shareMenu.getItemProps({ id: "whatsapp" })}>WhatsApp</li> </ul> </div> </Portal> </> ) }

Methods and Properties

The menu's api exposes the following methods:

  • isOpen — Whether the menu is open.
  • open(...) — Function to open the menu.
  • close(...) — Function to close the menu.
  • activeId — The id of the active menu item.
  • setActiveId(...) — Function to set the value active menu item.
  • isOptionChecked(...) — Function to check whether an option item is checked.

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.

Focused item state

When an item is focused, via keyboard navigation or pointer, it is given a data-focus attribute.

[data-part="item"][data-focus] { /* styles for focused state */ } [data-part="option-item"][data-focus] { /* styles for focused 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-checked attribute.

[data-part="option-item"][data-checked] { /* styles for checked state */ }

Edit this page on GitHub

On this page