Skip to main content
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/vue # or yarn add @zag-js/menu @zag-js/vue

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.
<script setup> import * as menu from "@zag-js/menu" import { normalizeProps, useMachine } from "@zag-js/vue" import { computed, onMounted, Teleport } from "vue" // Level 1 - File Menu const fileService = useMachine(menu.machine, { id: "1", "aria-label": "File", }) const fileMenu = computed(() => menu.connect(fileService, normalizeProps)) // Level 2 - Share Menu const shareService = useMachine(menu.machine, { id: "2", "aria-label": "Share", }) const shareMenu = computed(() => menu.connect(shareService, normalizeProps)) onMounted(() => { setTimeout(() => { fileMenu.value.setChild(shareService) shareMenu.value.setParent(fileService) }) }) // Share menu trigger const shareMenuTriggerProps = computed(() => fileMenu.value.getTriggerItemProps(shareMenu.value), ) </script> <template> <button v-bind="fileMenu.getTriggerProps()">Click me</button> <Teleport to="body"> <div v-bind="fileMenu.getPositionerProps()"> <ul ref="fileMenuRef" v-bind="fileMenu.getContentProps()"> <li v-bind="fileMenu.getItemProps({ value: 'new-tab' })">New tab</li> <li v-bind="fileMenu.getItemProps({ value: 'new-win' })">New window</li> <li v-bind="shareMenuTriggerProps">Share</li> <li v-bind="fileMenu.getItemProps({ value: 'print' })">Print...</li> <li v-bind="fileMenu.getItemProps({ value: 'help' })">Help</li> </ul> </div> </Teleport> <Teleport to="body"> <div v-bind="shareMenu.getPositionerProps()"> <ul ref="shareMenuRef" v-bind="shareMenu.getContentProps()"> <li v-bind="shareMenu.getItemProps({ value: 'messages' })">Messages</li> <li v-bind="shareMenu.getItemProps({ value: 'airdrop' })">Airdrop</li> <li v-bind="shareMenu.getItemProps({ value: 'whatsapp' })">WhatsApp</li> </ul> </div> </Teleport> </template>

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; 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 menu
  • closeOnSelectbooleanWhether to close the menu when an option is selected
  • aria-labelstringThe accessibility label for the menu
  • openbooleanThe controlled open state of the menu
  • onOpenChange(details: OpenChangeDetails) => voidFunction called when the menu opens or closes
  • defaultOpenbooleanThe 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 navigation
  • compositebooleanWhether the menu is a composed with other composite widgets like a combobox or tabs
  • navigate(details: NavigateDetails) => voidFunction to navigate to the selected item if it's an anchor element
  • 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: MenuService) => voidFunction to register a parent menu. This is used for submenus
  • setChild(child: MenuService) => 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
  • addItemListener(props: ItemListenerProps) => VoidFunctionSetup the custom event listener for item selection event

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-value
The value of the item
data-valuetext
The human-readable value
OptionItem
data-scope
menu
data-part
option-item
data-type
The type of the item
data-value
The value of the 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 © 2025
On this page