Skip to main content

An accessible dropdown and context menu that is used to display a list of actions or options that a user can choose.

Loading...

Features

  • Supports 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

Install the menu package:

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

Anatomy

Check the menu anatomy and part names.

Each part includes a data-part attribute to help identify them in the DOM.

Usage

Import the menu package:

import * as menu from "@zag-js/menu"

The menu package exports two key functions:

  • machine - State machine logic.
  • connect - Maps machine state to JSX props and event handlers.

Pass a unique id to useMachine so generated element ids stay predictable.

Then use the framework integration helpers:

<script setup> import * as menu from "@zag-js/menu" import { normalizeProps, useMachine } from "@zag-js/vue" import { computed } from "vue" const service = useMachine(menu.machine, { id: "1", "aria-label": "File" }) const api = computed(() => menu.connect(service, normalizeProps)) </script> <template> <div ref="ref"> <button v-bind="api.getTriggerProps()"> Actions <span v-bind="api.getIndicatorProps()"></span> </button> <div v-bind="api.getPositionerProps()"> <ul v-bind="api.getContentProps()"> <li v-bind="api.getItemProps({ value: 'edit' })">Edit</li> <li v-bind="api.getItemProps({ value: 'duplicate' })">Duplicate</li> <li v-bind="api.getItemProps({ value: 'delete' })">Delete</li> <li v-bind="api.getItemProps({ value: 'export' })">Export...</li> </ul> </div> </div> </template>

Listening for item selection

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

const service = 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 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 visibility externally.

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

Listening for highlighted items

Use onHighlightChange to react when highlighted item changes.

const service = useMachine(menu.machine, { onHighlightChange(details) { // details => { highlightedValue: string | null } console.log(details.highlightedValue) }, })

Setting initial highlighted item

Use defaultHighlightedValue to set the initially highlighted item.

const service = useMachine(menu.machine, { defaultHighlightedValue: "settings", })

Grouping menu items

When you have many menu items, it can help to group related options:

  • Wrap the menu items within an element.
  • Spread api.getGroupProps(...) props on the group element, providing an id.
  • Render a label for the menu group, providing the id of the group element.
//... <div {...api.getContentProps()}> {/* ... */} <hr {...api.getSeparatorProps()} /> <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> </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.

  • type — The type of option item. Either "checkbox" or "radio".
  • value — The value of the option item.
  • checked — The checked state of the option item.
  • onCheckedChange — The callback to invoke when the checked state changes.
<script setup> import * as menu from "@zag-js/menu" import { normalizeProps, useMachine } from "@zag-js/vue" import { computed, ref } from "vue" const data = { order: [ { label: "Ascending", value: "asc" }, { label: "Descending", value: "desc" }, { label: "None", value: "none" }, ], type: [ { label: "Email", value: "email" }, { label: "Phone", value: "phone" }, { label: "Address", value: "address" }, ], } const orderRef = ref("") const typeRef = ref([]) const service = useMachine(menu.machine, { id: "1" }) const api = computed(() => menu.connect(service, normalizeProps)) const radios = computed(() => data.order.map((item) => ({ label: item.label, id: item.value, type: "radio", value: item.value, checked: item.value === orderRef.value, onCheckedChange(v) { orderRef.value = v ? item.value : "" }, })), ) const checkboxes = computed(() => data.type.map((item) => ({ id: item.value, label: item.label, type: "checkbox", value: item.value, checked: typeRef.value.includes(item.value), onCheckedChange(v) { typeRef.value = v ? [...typeRef.value, item.value] : typeRef.value.filter((x) => x !== item.value) }, })), ) </script> <template> <button ref="ref" v-bind="api.getTriggerProps()">Trigger</button> <div v-bind="api.getPositionerProps()"> <div v-bind="api.getContentProps()"> <div v-for="item in radios" :key="item.value" v-bind="api.getOptionItemProps(item)" > <span v-bind="api.getItemIndicatorProps(item)"></span> <span v-bind="api.getItemTextProps(item)">{{ item.label }}</span> </div> <hr v-bind="api.getSeparatorProps()" /> <div v-for="item in checkboxes" :key="item.value" v-bind="api.getOptionItemProps(item)" > <span v-bind="api.getItemIndicatorProps(item)"></span> <span v-bind="api.getItemTextProps(item)">{{ item.label }}</span> </div> </div> </div> </template>

Default open state

Use defaultOpen to start with the menu opened in uncontrolled mode.

const service = useMachine(menu.machine, { defaultOpen: true, })

Keeping menu open after selection

Set closeOnSelect to false to keep the menu open after selecting an item.

const service = useMachine(menu.machine, { closeOnSelect: false, })

Positioning the menu

Use positioning to configure menu placement.

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

Labeling the menu without visible text

If you do not render a visible label, provide aria-label.

const service = useMachine(menu.machine, { "aria-label": "Actions", })

Styling guide

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

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="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
  • onRequestDismiss(event: LayerDismissEvent) => voidFunction called when this layer is closed due to a parent layer being closed
  • 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: ParentMenuService) => voidFunction to register a parent menu. This is used for submenus
  • setChild(child: ChildMenuService) => 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

ContextTrigger
data-scope
menu
data-part
context-trigger
data-state
"open" | "closed"
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-nested
menu
data-has-nested
menu
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"
ItemText
data-scope
menu
data-part
item-text
data-disabled
Present when disabled
data-highlighted
Present when highlighted
data-state
"checked"

CSS Variables

Arrow
--arrow-size
The size of the arrow
--arrow-size-half
Half the size of the arrow
--arrow-background
Use this variable to style the arrow background
--arrow-offset
The offset position of the arrow
Positioner
--reference-width
The width of the reference element
--reference-height
The height of the root
--available-width
The available width in viewport
--available-height
The available height in viewport
--x
The x position for transform
--y
The y position for transform
--z-index
The z-index value
--transform-origin
The transform origin for animations
Content
--layer-index
The index of the dismissable in the layer stack
--nested-layer-count
The number of nested menus
Backdrop
--layer-index
The index of the dismissable in the layer stack

Accessibility

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

Keyboard Interactions

  • Space
    Activates/Selects the highlighted item
  • Enter
    Activates/Selects the highlighted item
  • ArrowDown
    Highlights the next item in the menu
  • ArrowUp
    Highlights the previous item in the menu
  • ArrowRightArrowLeft
    When focus is on trigger, opens or closes the submenu depending on reading direction.
  • Esc
    Closes the menu and moves focus to the trigger
Edit this page on GitHub