Skip to main content
A menu is an accessible dropdown or context menu used to display a list of actions or options. It uses the aria-activedescendant pattern for focus management and supports typeahead navigation, grouped items, and radio/checkbox option items. Features
  • Supports items, labels, and groups
  • Focus managed with aria-activedescendant
  • Typeahead to focus items by typing
  • Full keyboard navigation including arrows, Home, End, Page Up/Down
  • Radio and checkbox option items
  • Submenu support
  • Controlled and uncontrolled open state

Installation

npm install @zag-js/menu @zag-js/react

Usage

Import the menu package and connect the machine to your framework:
import * as menu from "@zag-js/menu"
import { useMachine, normalizeProps } from "@zag-js/react"
import { useId } from "react"
The menu package exports two key functions:
  • machine — State machine logic.
  • connect — Maps machine state to JSX props and event handlers.
import * as menu from "@zag-js/menu"
import { useMachine, normalizeProps } from "@zag-js/react"
import { useId } from "react"

export function Menu() {
  const service = useMachine(menu.machine, {
    id: useId(),
    onSelect(details) {
      console.log("selected:", details.value)
    },
  })

  const api = menu.connect(service, normalizeProps)

  return (
    <div>
      <button {...api.getTriggerProps()}>Actions â–¼</button>

      <div {...api.getPositionerProps()}>
        <div {...api.getContentProps()}>
          <div {...api.getItemProps({ value: "edit" })}>Edit</div>
          <div {...api.getItemProps({ value: "duplicate" })}>Duplicate</div>
          <hr {...api.getSeparatorProps()} />
          <div {...api.getItemProps({ value: "delete", disabled: true })}>
            Delete
          </div>
        </div>
      </div>
    </div>
  )
}

Listening for item selection

Use onSelect to react when a menu item is clicked.
const service = useMachine(menu.machine, {
  onSelect(details) {
    // details => { value: string }
    console.log("selected value is:", details.value)
  },
})

Listening for open state changes

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 menu visibility externally.
const [open, setOpen] = useState(false)

const service = useMachine(menu.machine, {
  open,
  onOpenChange(details) {
    setOpen(details.open)
  },
})

Default open state

Use defaultOpen to start the menu open in uncontrolled mode.
const service = useMachine(menu.machine, {
  defaultOpen: true,
})

Grouping menu items

Group related items with getItemGroupProps and getItemGroupLabelProps.
<div {...api.getContentProps()}>
  <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>
  <hr {...api.getSeparatorProps()} />
  <button {...api.getItemProps({ value: "settings" })}>Settings</button>
</div>

Checkbox and radio option items

Use getOptionItemProps with type: "checkbox" or type: "radio" for toggleable menu items.
const [sort, setSort] = useState("asc")
const [filters, setFilters] = useState({ starred: false, archived: false })

// Radio item — only one can be selected at a time
<div {...api.getOptionItemProps({
  type: "radio",
  value: "asc",
  checked: sort === "asc",
  onCheckedChange: () => setSort("asc"),
})}>
  Ascending
</div>

<div {...api.getOptionItemProps({
  type: "radio",
  value: "desc",
  checked: sort === "desc",
  onCheckedChange: () => setSort("desc"),
})}>
  Descending
</div>

// Checkbox item — multiple can be selected
<div {...api.getOptionItemProps({
  type: "checkbox",
  value: "starred",
  checked: filters.starred,
  onCheckedChange: (checked) => setFilters((f) => ({ ...f, starred: checked })),
})}>
  Starred
</div>

Keeping the menu open after selection

Set closeOnSelect to false to keep the menu open after selecting an item.
const service = useMachine(menu.machine, {
  closeOnSelect: false,
})

Setting initial highlighted item

Use defaultHighlightedValue to set the initially highlighted item.
const service = useMachine(menu.machine, {
  defaultHighlightedValue: "settings",
})

Positioning

const service = useMachine(menu.machine, {
  positioning: { placement: "bottom-start" },
})

Context menu

Use getContextTriggerProps to attach the menu to a context menu (right-click) trigger.
<div {...api.getContextTriggerProps()}>
  Right-click here
</div>

<div {...api.getPositionerProps()}>
  <div {...api.getContentProps()}>
    {/* menu items */}
  </div>
</div>

Labeling without visible text

If no visible trigger label exists, provide aria-label.
const service = useMachine(menu.machine, {
  "aria-label": "File actions",
})

API Reference

open
boolean
The controlled open state of the menu.
defaultOpen
boolean
The initial open state when rendered.
closeOnSelect
boolean
default:"true"
Whether to close the menu when an item is selected.
loopFocus
boolean
default:"false"
Whether keyboard navigation loops from last item to first and vice versa.
typeahead
boolean
default:"true"
Whether pressing printable characters triggers typeahead navigation.
highlightedValue
string | null
The controlled highlighted menu item value.
defaultHighlightedValue
string | null
The initial highlighted item value when rendered.
positioning
PositioningOptions
Options for dynamically positioning the menu content.
aria-label
string
Accessible label for the menu when no visible label is rendered.
onSelect
(details: { value: string }) => void
Callback invoked when a menu item is selected.
onHighlightChange
(details: { highlightedValue: string | null }) => void
Callback invoked when the highlighted item changes.
onOpenChange
(details: { open: boolean }) => void
Callback invoked when the menu opens or closes.

Styling

Each menu part includes a data-part attribute you can target in CSS.

Parts

PartDescription
triggerThe button that opens the menu
context-triggerThe right-click context menu trigger area
positionerPositions the menu content
contentThe menu panel
itemAn individual menu item
item-textThe text inside a menu item
item-indicatorChecked indicator for option items
separatorA visual divider between items or groups
arrowOptional arrow element

Open and closed state

[data-part="content"][data-state="open"] {
  /* styles for open state */
}

[data-part="content"][data-state="closed"] {
  /* styles for closed state */
}

[data-part="trigger"][data-state="open"] {
  /* styles when the menu is open */
}

Highlighted item state

When a menu item is highlighted via keyboard or pointer, data-highlighted is added.
[data-part="item"][data-highlighted] {
  background: var(--highlight-color);
}

[data-part="item"][data-type="radio"][data-highlighted] {
  background: var(--highlight-color);
}

[data-part="item"][data-type="checkbox"][data-highlighted] {
  background: var(--highlight-color);
}

Disabled item state

[data-part="item"][data-disabled] {
  opacity: 0.4;
  cursor: not-allowed;
}

Checked option item state

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

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

Arrow

Style the arrow using CSS variables.
[data-part="arrow"] {
  --arrow-size: 8px;
  --arrow-background: white;
}

Accessibility

Uses the aria-activedescendant pattern to manage focus movement among menu items without moving DOM focus.
  • The menu trigger has aria-haspopup="menu" and aria-expanded.
  • The menu content has role="menu".
  • Each item has role="menuitem", role="menuitemradio", or role="menuitemcheckbox".

Keyboard interactions

KeyDescription
Enter / SpaceOpens the menu (on trigger) or activates the highlighted item.
EscapeCloses the menu and returns focus to the trigger.
ArrowDownOpens the menu and moves highlight to the first item; moves to the next item when open.
ArrowUpOpens the menu and moves highlight to the last item; moves to the previous item when open.
HomeMoves highlight to the first item.
EndMoves highlight to the last item.
TabCloses the menu and moves focus to the next focusable element.
Printable charactersMoves highlight to the next item starting with the typed character (typeahead).

Build docs developers (and LLMs) love