{"slug":"menu","title":"Menu","description":"Using the menu machine in your project.","contentType":"component","framework":"svelte","content":"An accessible dropdown and context menu that is used to display a list of\nactions or options that a user can choose.\n\n## Resources\n\n\n[Latest version: v1.39.1](https://www.npmjs.com/package/@zag-js/menu)\n[Logic Visualizer](https://zag-visualizer.vercel.app/menu)\n[Source Code](https://github.com/chakra-ui/zag/tree/main/packages/machines/menu)\n\n\n\n**Features**\n\n- Supports items, labels, groups of items\n- Focus is fully managed using `aria-activedescendant` pattern\n- Typeahead to allow focusing items by typing text\n- Keyboard navigation support including arrow keys, home/end, page up/down\n- Supports multiple triggers sharing a single menu instance\n\n## Installation\n\nInstall the menu package:\n\n```bash\nnpm install @zag-js/menu @zag-js/svelte\n# or\nyarn add @zag-js/menu @zag-js/svelte\n```\n\n## Anatomy\n\nCheck the menu anatomy and part names.\n\n> Each part includes a `data-part` attribute to help identify them in the DOM.\n\n\n\n## Usage\n\nImport the menu package:\n\n```tsx\nimport * as menu from \"@zag-js/menu\"\n```\n\nThe menu package exports two key functions:\n\n- `machine` - State machine logic.\n- `connect` - Maps machine state to JSX props and event handlers.\n\n> Pass a unique `id` to `useMachine` so generated element ids stay predictable.\n\nThen use the framework integration helpers:\n\n```svelte\n<script lang=\"ts\">\n  import * as menu from \"@zag-js/menu\"\n  import { portal, useMachine, normalizeProps } from \"@zag-js/svelte\"\n\n  const id = $props.id()\n  const service = useMachine(menu.machine, { id })\n  const api = $derived(menu.connect(service, normalizeProps))\n</script>\n\n<div>\n  <button {...api.getTriggerProps()}>\n    Actions <span {...api.getIndicatorProps()}>▾</span>\n  </button>\n  <div use:portal {...api.getPositionerProps()}>\n    <ul {...api.getContentProps()}>\n      <li {...api.getItemProps({ value: \"edit\" })}>Edit</li>\n      <li {...api.getItemProps({ value: \"duplicate\" })}>Duplicate</li>\n      <li {...api.getItemProps({ value: \"delete\" })}>Delete</li>\n      <li {...api.getItemProps({ value: \"export\" })}>Export...</li>\n    </ul>\n  </div>\n</div>\n```\n\n### Listening for item selection\n\nWhen a menu item is clicked, the `onSelect` callback is invoked.\n\n```tsx {3-6}\nconst service = useMachine(menu.machine, {\n  onSelect(details) {\n    // details => { value: string }\n    console.log(\"selected value is \", details.value)\n  },\n})\n```\n\n### Listening for open state changes\n\nWhen a menu is opened or closed, the `onOpenChange` callback is invoked.\n\n```tsx {3-6}\nconst service = useMachine(menu.machine, {\n  onOpenChange(details) {\n    // details => { open: boolean }\n    console.log(\"open state is \", details.open)\n  },\n})\n```\n\n### Controlled open state\n\nUse `open` and `onOpenChange` to control visibility externally.\n\n```tsx\nconst service = useMachine(menu.machine, {\n  open,\n  onOpenChange(details) {\n    setOpen(details.open)\n  },\n})\n```\n\n### Listening for highlighted items\n\nUse `onHighlightChange` to react when highlighted item changes.\n\n```tsx\nconst service = useMachine(menu.machine, {\n  onHighlightChange(details) {\n    // details => { highlightedValue: string | null }\n    console.log(details.highlightedValue)\n  },\n})\n```\n\n### Setting initial highlighted item\n\nUse `defaultHighlightedValue` to set the initially highlighted item.\n\n```tsx\nconst service = useMachine(menu.machine, {\n  defaultHighlightedValue: \"settings\",\n})\n```\n\n### Grouping menu items\n\nWhen you have many menu items, it can help to group related options:\n\n- Wrap the menu items within an element.\n- Spread `api.getGroupProps(...)` props on the group element, providing an `id`.\n- Render a label for the menu group, providing the `id` of the group element.\n\n```tsx\n//...\n<div {...api.getContentProps()}>\n  {/* ... */}\n  <hr {...api.getSeparatorProps()} />\n  <p {...api.getItemGroupLabelProps({ htmlFor: \"account\" })}>Accounts</p>\n  <div {...api.getItemGroupProps({ id: \"account\" })}>\n    <button {...api.getItemProps({ value: \"account-1\" })}>Account 1</button>\n    <button {...api.getItemProps({ value: \"account-2\" })}>Account 2</button>\n  </div>\n</div>\n//...\n```\n\n### Checkbox and Radio option items\n\nTo use checkbox or radio option items, you'll need to:\n\n- Add a `value` property to the machine's context whose value is an object\n  describing the state of the option items.\n- Use the `api.getOptionItemProps(...)` function to get the props for the option\n  item.\n\nA common requirement for the option item that you pass the `name`, `value` and\n`type` properties.\n\n- `type` — The type of option item. Either `\"checkbox\"` or `\"radio\"`.\n- `value` — The value of the option item.\n- `checked` — The checked state of the option item.\n- `onCheckedChange` — The callback to invoke when the checked state changes.\n\n```svelte\n<script lang=\"ts\">\n  import * as menu from \"@zag-js/menu\"\n  import { portal, useMachine, normalizeProps } from \"@zag-js/svelte\"\n\n  const data = {\n    order: [\n      { label: \"Ascending\", value: \"asc\" },\n      { label: \"Descending\", value: \"desc\" },\n      { label: \"None\", value: \"none\" },\n    ],\n    type: [\n      { label: \"Email\", value: \"email\" },\n      { label: \"Phone\", value: \"phone\" },\n      { label: \"Address\", value: \"address\" },\n    ],\n  }\n\n  let order = $state(\"\")\n  let type = $state<string[]>([])\n\n  const id = $props.id()\n  const service = useMachine(menu.machine, { id })\n  const api = $derived(menu.connect(service, normalizeProps))\n\n  const radios = $derived(\n    data.order.map((item) => ({\n      type: \"radio\" as const,\n      name: \"order\",\n      value: item.value,\n      label: item.label,\n      checked: order === item.value,\n      onCheckedChange: (checked: boolean) => {\n        order = checked ? item.value : \"\"\n      },\n    })),\n  )\n\n  const checkboxes = $derived(\n    data.type.map((item) => ({\n      type: \"checkbox\" as const,\n      name: \"type\",\n      value: item.value,\n      label: item.label,\n      checked: type.includes(item.value),\n      onCheckedChange: (checked: boolean) => {\n        type = checked ? [...type, item.value] : type.filter((x) => x !== item.value)\n      },\n    })),\n  )\n</script>\n\n<button {...api.getTriggerProps()}> Trigger </button>\n\n<div use:portal {...api.getPositionerProps()}>\n  <div {...api.getContentProps()}>\n    {#each radios as item}\n      <div {...api.getOptionItemProps(item)}>\n        <span {...api.getItemIndicatorProps(item)}>✅</span>\n        <span {...api.getItemTextProps(item)}>{item.label}</span>\n      </div>\n    {/each}\n    <hr {...api.getSeparatorProps()} />\n    {#each checkboxes as item}\n      <div {...api.getOptionItemProps(item)}>\n        <span {...api.getItemIndicatorProps(item)}>✅</span>\n        <span {...api.getItemTextProps(item)}>{item.label}</span>\n      </div>\n    {/each}\n  </div>\n</div>\n```\n\n### Default open state\n\nUse `defaultOpen` to start with the menu opened in uncontrolled mode.\n\n```tsx\nconst service = useMachine(menu.machine, {\n  defaultOpen: true,\n})\n```\n\n### Keeping menu open after selection\n\nSet `closeOnSelect` to `false` to keep the menu open after selecting an item.\n\n```tsx\nconst service = useMachine(menu.machine, {\n  closeOnSelect: false,\n})\n```\n\n### Positioning the menu\n\nUse `positioning` to configure menu placement.\n\n```tsx\nconst service = useMachine(menu.machine, {\n  positioning: { placement: \"bottom-start\" },\n})\n```\n\n### Multiple triggers\n\nA single menu instance can be shared across multiple trigger elements. Pass a\n`value` to `getTriggerProps` to identify each trigger.\n\n```tsx\nconst service = useMachine(menu.machine, {\n  onTriggerValueChange({ value }) {\n    console.log(\"active trigger:\", value)\n  },\n})\n\nconst api = menu.connect(service, normalizeProps)\n\nreturn (\n  <>\n    <button {...api.getTriggerProps({ value: \"file\" })}>File</button>\n    <button {...api.getTriggerProps({ value: \"edit\" })}>Edit</button>\n    <div {...api.getPositionerProps()}>\n      <div {...api.getContentProps()}>\n        {/* Menu items based on api.triggerValue */}\n      </div>\n    </div>\n  </>\n)\n```\n\nWhen the menu is open and a different trigger is activated, it repositions\nwithout closing. `aria-expanded` is scoped to the active trigger, and focus\nreturns to the correct trigger on close.\n\n### Labeling the menu without visible text\n\nIf you do not render a visible label, provide `aria-label`.\n\n```tsx\nconst service = useMachine(menu.machine, {\n  \"aria-label\": \"Actions\",\n})\n```\n\n## Styling guide\n\nEach menu part includes a `data-part` attribute you can target in CSS.\n\n### Open and closed state\n\nWhen the menu is open or closed, the content and trigger parts will have the\n`data-state` attribute.\n\n```css\n[data-part=\"content\"][data-state=\"open|closed\"] {\n  /* styles for open or closed state */\n}\n\n[data-part=\"trigger\"][data-state=\"open|closed\"] {\n  /* styles for open or closed state */\n}\n```\n\n### Highlighted item state\n\nWhen an item is highlighted, via keyboard navigation or pointer, it is given a\n`data-highlighted` attribute.\n\n```css\n[data-part=\"item\"][data-highlighted] {\n  /* styles for highlighted state */\n}\n\n[data-part=\"item\"][data-type=\"radio|checkbox\"][data-highlighted] {\n  /* styles for highlighted state */\n}\n```\n\n### Disabled item state\n\nWhen an item or an option item is disabled, it is given a `data-disabled`\nattribute.\n\n```css\n[data-part=\"item\"][data-disabled] {\n  /* styles for disabled state */\n}\n\n[data-part=\"item\"][data-type=\"radio|checkbox\"][data-disabled] {\n  /* styles for disabled state */\n}\n```\n\n### Using arrows\n\nWhen using arrows within the menu, you can style it using CSS variables.\n\n```css\n[data-part=\"arrow\"] {\n  --arrow-size: 20px;\n  --arrow-background: red;\n}\n```\n\n### Checked option item state\n\nWhen an option item is checked, it is given a `data-state` attribute.\n\n```css\n[data-part=\"item\"][data-type=\"radio|checkbox\"][data-state=\"checked\"] {\n  /* styles for checked state */\n}\n```\n\n## Methods and Properties\n\n### Machine Context\n\nThe menu machine exposes the following context properties:\n\n**`ids`**\nType: `Partial<{ trigger: string | ((value?: string) => string); contextTrigger: string | ((value?: string) => string); content: string; groupLabel: (id: string) => string; group: (id: string) => string; positioner: string; arrow: string; }>`\nDescription: The ids of the elements in the menu. Useful for composition.\n\n**`defaultHighlightedValue`**\nType: `string`\nDescription: The initial highlighted value of the menu item when rendered.\nUse when you don't need to control the highlighted value of the menu item.\n\n**`highlightedValue`**\nType: `string`\nDescription: The controlled highlighted value of the menu item.\n\n**`onHighlightChange`**\nType: `(details: HighlightChangeDetails) => void`\nDescription: Function called when the highlighted menu item changes.\n\n**`onSelect`**\nType: `(details: SelectionDetails) => void`\nDescription: Function called when a menu item is selected.\n\n**`anchorPoint`**\nType: `Point`\nDescription: The positioning point for the menu. Can be set by the context menu trigger or the button trigger.\n\n**`loopFocus`**\nType: `boolean`\nDescription: Whether to loop the keyboard navigation.\n\n**`positioning`**\nType: `PositioningOptions`\nDescription: The options used to dynamically position the menu\n\n**`closeOnSelect`**\nType: `boolean`\nDescription: Whether to close the menu when an option is selected\n\n**`aria-label`**\nType: `string`\nDescription: The accessibility label for the menu\n\n**`open`**\nType: `boolean`\nDescription: The controlled open state of the menu\n\n**`onOpenChange`**\nType: `(details: OpenChangeDetails) => void`\nDescription: Function called when the menu opens or closes\n\n**`defaultOpen`**\nType: `boolean`\nDescription: The initial open state of the menu when rendered.\nUse when you don't need to control the open state of the menu.\n\n**`typeahead`**\nType: `boolean`\nDescription: Whether the pressing printable characters should trigger typeahead navigation\n\n**`composite`**\nType: `boolean`\nDescription: Whether the menu is a composed with other composite widgets like a combobox or tabs\n\n**`navigate`**\nType: `(details: NavigateDetails) => void`\nDescription: Function to navigate to the selected item if it's an anchor element\n\n**`triggerValue`**\nType: `string`\nDescription: The controlled trigger value\n\n**`defaultTriggerValue`**\nType: `string`\nDescription: The initial trigger value when rendered.\nUse when you don't need to control the trigger value.\n\n**`onTriggerValueChange`**\nType: `(details: TriggerValueChangeDetails) => void`\nDescription: Function called when the trigger value changes.\n\n**`dir`**\nType: `\"ltr\" | \"rtl\"`\nDescription: The document's text/writing direction.\n\n**`id`**\nType: `string`\nDescription: The unique identifier of the machine.\n\n**`getRootNode`**\nType: `() => ShadowRoot | Node | Document`\nDescription: A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron.\n\n**`onEscapeKeyDown`**\nType: `(event: KeyboardEvent) => void`\nDescription: Function called when the escape key is pressed\n\n**`onRequestDismiss`**\nType: `(event: LayerDismissEvent) => void`\nDescription: Function called when this layer is closed due to a parent layer being closed\n\n**`onPointerDownOutside`**\nType: `(event: PointerDownOutsideEvent) => void`\nDescription: Function called when the pointer is pressed down outside the component\n\n**`onFocusOutside`**\nType: `(event: FocusOutsideEvent) => void`\nDescription: Function called when the focus is moved outside the component\n\n**`onInteractOutside`**\nType: `(event: InteractOutsideEvent) => void`\nDescription: Function called when an interaction happens outside the component\n\n### Machine API\n\nThe menu `api` exposes the following methods:\n\n**`open`**\nType: `boolean`\nDescription: Whether the menu is open\n\n**`setOpen`**\nType: `(open: boolean) => void`\nDescription: Function to open or close the menu\n\n**`triggerValue`**\nType: `string`\nDescription: The trigger value\n\n**`setTriggerValue`**\nType: `(value: string) => void`\nDescription: Function to set the trigger value\n\n**`highlightedValue`**\nType: `string`\nDescription: The id of the currently highlighted menuitem\n\n**`setHighlightedValue`**\nType: `(value: string) => void`\nDescription: Function to set the highlighted menuitem\n\n**`setParent`**\nType: `(parent: ParentMenuService) => void`\nDescription: Function to register a parent menu. This is used for submenus\n\n**`setChild`**\nType: `(child: ChildMenuService) => void`\nDescription: Function to register a child menu. This is used for submenus\n\n**`reposition`**\nType: `(options?: Partial<PositioningOptions>) => void`\nDescription: Function to reposition the popover\n\n**`getOptionItemState`**\nType: `(props: OptionItemProps) => OptionItemState`\nDescription: Returns the state of the option item\n\n**`getItemState`**\nType: `(props: ItemProps) => ItemState`\nDescription: Returns the state of the menu item\n\n**`addItemListener`**\nType: `(props: ItemListenerProps) => VoidFunction`\nDescription: Setup the custom event listener for item selection event\n\n### Data Attributes\n\n**`ContextTrigger`**\n\n**`data-scope`**: menu\n**`data-part`**: context-trigger\n**`data-value`**: The value of the item\n**`data-current`**: Present when current\n**`data-state`**: \"open\" | \"closed\"\n\n**`Trigger`**\n\n**`data-scope`**: menu\n**`data-part`**: trigger\n**`data-placement`**: The placement of the trigger\n**`data-value`**: The value of the item\n**`data-current`**: Present when current\n**`data-controls`**: \n**`data-state`**: \"open\" | \"closed\"\n\n**`Indicator`**\n\n**`data-scope`**: menu\n**`data-part`**: indicator\n**`data-state`**: \"open\" | \"closed\"\n\n**`Content`**\n\n**`data-scope`**: menu\n**`data-part`**: content\n**`data-state`**: \"open\" | \"closed\"\n**`data-nested`**: menu\n**`data-has-nested`**: menu\n**`data-placement`**: The placement of the content\n\n**`Item`**\n\n**`data-scope`**: menu\n**`data-part`**: item\n**`data-disabled`**: Present when disabled\n**`data-highlighted`**: Present when highlighted\n**`data-value`**: The value of the item\n**`data-valuetext`**: The human-readable value\n\n**`OptionItem`**\n\n**`data-scope`**: menu\n**`data-part`**: option-item\n**`data-type`**: The type of the item\n**`data-value`**: The value of the item\n**`data-state`**: \"checked\" | \"unchecked\"\n**`data-disabled`**: Present when disabled\n**`data-highlighted`**: Present when highlighted\n**`data-valuetext`**: The human-readable value\n\n**`ItemIndicator`**\n\n**`data-scope`**: menu\n**`data-part`**: item-indicator\n**`data-disabled`**: Present when disabled\n**`data-highlighted`**: Present when highlighted\n**`data-state`**: \"checked\"\n\n**`ItemText`**\n\n**`data-scope`**: menu\n**`data-part`**: item-text\n**`data-disabled`**: Present when disabled\n**`data-highlighted`**: Present when highlighted\n**`data-state`**: \"checked\"\n\n### CSS Variables\n\n<CssVarTable name=\"menu\" />\n\n## Accessibility\n\nUses\n[aria-activedescendant](https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/examples/menu-button-actions-active-descendant/)\npattern to manage focus movement among menu items.\n\n### Keyboard Interactions\n\n**`Space`**\nDescription: Activates/Selects the highlighted item\n\n**`Enter`**\nDescription: Activates/Selects the highlighted item\n\n**`ArrowDown`**\nDescription: Highlights the next item in the menu\n\n**`ArrowUp`**\nDescription: Highlights the previous item in the menu\n\n**`ArrowRight + ArrowLeft`**\nDescription: <span>When focus is on trigger, opens or closes the submenu depending on reading direction.</span>\n\n**`Esc`**\nDescription: Closes the menu and moves focus to the trigger","package":"@zag-js/menu","editUrl":"https://github.com/chakra-ui/zag/edit/main/website/data/components/menu.mdx"}