Skip to main content
0.57.0
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.

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 ๐Ÿ”ฅ

  • 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 * as menu from "@zag-js/menu" import { useMachine, normalizeProps, Portal } 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.getTriggerProps()}>Click me</button> <Portal> <div {...fileMenu.getPositionerProps()}> <ul {...fileMenu.getContentProps()}> <li {...fileMenu.getItemProps({ value: "new-tab" })}>New tab</li> <li {...fileMenu.getItemProps({ value: "new-win" })}>New window</li> <li {...shareMenuTriggerProps}>Share</li> <li {...fileMenu.getItemProps({ value: "print" })}>Print...</li> <li {...fileMenu.getItemProps({ value: "help" })}>Help</li> </ul> </div> </Portal> <Portal> <div {...shareMenu.getPositionerProps()}> <ul {...shareMenu.getContentProps()}> <li {...shareMenu.getItemProps({ value: "messages" })}>Messages</li> <li {...shareMenu.getItemProps({ value: "airdrop" })}>Airdrop</li> <li {...shareMenu.getItemProps({ value: "whatsapp" })}>WhatsApp</li> </ul> </div> </Portal> </> ) }

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="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; label(id: string): string; group(id: string): string; positioner: string; arrow: string; }>The ids of the elements in the menu. Useful for composition.
  • highlightedValuestringThe value of the highlighted 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 menu
  • closeOnSelectbooleanWhether to close the menu when an option is selected
  • aria-labelstringThe accessibility label for the menu
  • openbooleanWhether the menu is open
  • onOpenChange(details: OpenChangeDetails) => voidFunction called when the menu opens or closes
  • open.controlledbooleanWhether the menu's open state is controlled by the user
  • typeaheadbooleanWhether the pressing printable characters should trigger typeahead navigation
  • compositebooleanWhether the menu is a composed with other composite widgets like a combobox or tabs
  • 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.
  • onEscapeKeyDown(event: KeyboardEvent) => voidFunction called when the escape key is pressed
  • onPointerDownOutside(event: PointerDownOutsideEvent) => voidFunction called when the pointer is pressed down outside the component
  • onFocusOutside(event: FocusOutsideEvent) => voidFunction called when the focus is moved outside the component
  • onInteractOutside(event: InteractOutsideEvent) => voidFunction called when an interaction happens outside the component

Machine API

The menu api exposes the following methods:

  • openbooleanWhether the menu is open
  • setOpen(open: boolean) => voidFunction to open or close the menu
  • highlightedValuestringThe id of the currently highlighted menuitem
  • setHighlightedValue(value: 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
  • 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

Data Attributes

Trigger
data-scope
menu
data-part
trigger
data-placement
The placement of the trigger
data-state
"open" | "closed"
Indicator
data-scope
menu
data-part
indicator
data-state
"open" | "closed"
Content
data-scope
menu
data-part
content
data-state
"open" | "closed"
data-placement
The placement of the content
Item
data-scope
menu
data-part
item
data-disabled
Present when disabled
data-highlighted
Present when highlighted
data-valuetext
The human-readable value
OptionItem
data-scope
menu
data-part
option-item
data-state
"checked" | "unchecked"
data-disabled
Present when disabled
data-highlighted
Present when highlighted
data-valuetext
The human-readable value
ItemIndicator
data-scope
menu
data-part
item-indicator
data-disabled
Present when disabled
data-highlighted
Present when highlighted
data-state
"checked" | "unchecked"
ItemText
data-scope
menu
data-part
item-text
data-disabled
Present when disabled
data-highlighted
Present when highlighted
data-state
"checked" | "unchecked"

Accessibility

Uses aria-activedescendant pattern to manage focus movement among menu items.

Keyboard Interactions

  • Space
    Opens/closes the nested menu.
  • Enter
    Opens/closes the nested menu.
  • ArrowDown
    Moves focus to the next item.
  • ArrowUp
    Moves focus to the previous item.
  • ArrowRight
    Opens the nested menu.
  • ArrowLeft
    Closes the nested menu.
  • Esc
    Closes the nested menu and moves focus to the parent menu item.

Edit this page on GitHub

Proudly made in๐Ÿ‡ณ๐Ÿ‡ฌby Segun Adebayo

Copyright © 2024
On this page