Navigation Menu
An accessible navigation menu component that provides a list of links with optional dropdown content. Supports keyboard navigation, hover/click interactions, animated indicators, and follows WAI-ARIA practices.
Features
- Support for basic (inline content) and viewport (shared viewport) patterns
- Hover and click trigger support with configurable delays
- Keyboard navigation with arrow keys, Tab, Home/End
- Animated indicator that follows the active trigger
- Smooth content animations with viewport positioning
- Support for nested links within dropdown content
- Horizontal and vertical orientation
- RTL (right-to-left) support
- Fully managed focus and tab order
- Dismissible with click outside or Escape key
Installation
To use the navigation menu machine in your project, run the following command in your command line:
Anatomy
To set up the navigation menu correctly, you'll need to understand its anatomy and how we name its parts.
Each part includes a
data-partattribute to help identify them in the DOM.
Usage
First, import the navigation menu package into your project
import * as navigationMenu from "@zag-js/navigation-menu"
The navigation menu package exports two key functions:
machine— The state machine logic for the navigation menu widget.connect— The function that translates the machine's state to JSX attributes and event handlers.
You'll need to provide a unique
idto theuseMachinehook. 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 navigation menu machine in your project 🔥
The basic pattern places content directly within each item. This is suitable for simple dropdown menus where each dropdown appears below its trigger.
Advanced pattern with viewport
The viewport pattern uses a shared viewport container for all content. This enables smooth transitions and better performance for complex navigation layouts.
In this pattern:
- Content is rendered inside a shared
viewportelement - The viewport automatically positions itself relative to the active trigger
- You must include
triggerProxyandviewportProxyfor proper focus management
When to use viewport pattern:
- Complex navigation with varying content sizes
- Smooth animated transitions between different content
- Header navigation bars (like on e-commerce sites)
- When you want a single shared container for all dropdowns
Controlling the navigation menu
To control which item is currently open, pass the value and onValueChange
properties to the machine.
Listening for value changes
When the open item changes, the onValueChange callback is invoked with the new
value.
const service = useMachine(navigationMenu.machine, { id: "nav", onValueChange(details) { // details => { value: string } console.log("Current open item:", details.value) }, })
Adding an animated indicator
To show a visual indicator that animates to the active trigger, render the indicator within the list container:
The indicator automatically transitions to match the active trigger's position and size using CSS variables.
Configuring hover delays
You can customize the delay before opening and closing on hover:
const service = useMachine(navigationMenu.machine, { openDelay: 300, // Delay before opening on hover (default: 200ms) closeDelay: 400, // Delay before closing on pointer leave (default: 300ms) })
Tip: Longer delays provide a more forgiving user experience but can feel less responsive.
Disabling hover or click triggers
You can disable hover or click triggers independently:
const service = useMachine(navigationMenu.machine, { disableHoverTrigger: true, // Only open on click // OR disableClickTrigger: true, // Only open on hover // OR disablePointerLeaveClose: true, // Prevents closing when pointer leaves })
disableHoverTrigger— Prevents opening on hover (click only)disableClickTrigger— Prevents opening on click (hover only)disablePointerLeaveClose— Prevents closing when pointer leaves
Changing orientation
The default orientation is horizontal. To create a vertical navigation menu:
const service = useMachine(navigationMenu.machine, { orientation: "vertical", })
This affects keyboard navigation (arrow keys) and indicator positioning.
Disabling items
To disable a navigation item, pass disabled: true to the item props:
<div {...api.getItemProps({ value: "products", disabled: true })}> <button {...api.getTriggerProps({ value: "products", disabled: true })}> Products </button> </div>
Disabled items cannot be opened and are skipped during keyboard navigation.
Indicating current page
To highlight the current page link, use the current prop:
<a {...api.getLinkProps({ value: "products", current: true })}>Products</a>
This adds data-current attribute and aria-current="page" for accessibility.
RTL support
The navigation menu supports right-to-left languages. Set the dir property to
rtl:
const service = useMachine(navigationMenu.machine, { dir: "rtl", })
Styling guide
Earlier, we mentioned that each navigation menu part has a data-part attribute
added to them to select and style them in the DOM.
Open and closed states
When content is open or closed, it receives a data-state attribute:
[data-part="content"][data-state="open|closed"] { /* Styles for open or closed content */ } [data-part="trigger"][data-state="open|closed"] { /* Styles for open or closed trigger */ } [data-part="viewport"][data-state="open|closed"] { /* Styles for viewport open/closed state */ }
Selected item state
When an item is selected (open), it receives data-state="open":
[data-part="item"][data-state="open"] { /* Styles for open item */ }
Disabled state
Disabled items have a data-disabled attribute:
[data-part="item"][data-disabled] { /* Styles for disabled items */ } [data-part="trigger"][data-disabled] { /* Styles for disabled triggers */ }
Orientation styles
All parts have a data-orientation attribute:
[data-part="root"][data-orientation="horizontal|vertical"] { /* Orientation-specific styles */ } [data-part="list"][data-orientation="horizontal"] { display: flex; flex-direction: row; } [data-part="list"][data-orientation="vertical"] { display: flex; flex-direction: column; }
Current link state
Links marked as current have a data-current attribute:
[data-part="link"][data-current] { /* Styles for current page link */ }
Styling the indicator
The indicator uses CSS variables for positioning and sizing:
[data-part="indicator"] { position: absolute; transition: translate 250ms ease, width 250ms ease, height 250ms ease; } [data-part="indicator"][data-orientation="horizontal"] { left: 0; translate: var(--trigger-x) 0; width: var(--trigger-width); } [data-part="indicator"][data-orientation="vertical"] { top: 0; translate: 0 var(--trigger-y); height: var(--trigger-height); }
Styling the viewport
The viewport uses CSS variables for positioning and sizing:
[data-part="viewport"] { position: absolute; width: var(--viewport-width); height: var(--viewport-height); transition: width 300ms ease, height 300ms ease; }
Arrow styling
The arrow can be styled using CSS variables:
[data-part="root"] { --arrow-size: 20px; } [data-part="arrow"] { width: var(--arrow-size); height: var(--arrow-size); background: white; rotate: 45deg; }
Motion attributes
When using the viewport pattern, content elements receive data-motion
attributes for directional animations:
[data-part="content"][data-motion="from-start"] { animation: slideFromStart 250ms ease; } [data-part="content"][data-motion="from-end"] { animation: slideFromEnd 250ms ease; } [data-part="content"][data-motion="to-start"] { animation: slideToStart 250ms ease; } [data-part="content"][data-motion="to-end"] { animation: slideToEnd 250ms ease; }
Tip: The motion direction indicates where the content is coming from (from-start/from-end) or going to (to-start/to-end), enabling context-aware animations when switching between items.
Methods and Properties
Machine Context
The navigation menu machine exposes the following context properties:
idsPartial<{ root: string; list: string; item: string; trigger: (value: string) => string; content: (value: string) => string; viewport: string; }>The ids of the elements in the machine.valuestringThe controlled value of the navigation menudefaultValuestringThe default value of the navigation menu. Use when you don't want to control the value of the menu.onValueChange(details: ValueChangeDetails) => voidFunction called when the value of the menu changesopenDelaynumberThe delay before the menu openscloseDelaynumberThe delay before the menu closesdisableClickTriggerbooleanWhether to disable the click triggerdisableHoverTriggerbooleanWhether to disable the hover triggerdisablePointerLeaveClosebooleanWhether to disable the pointer leave closedir"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.orientation"horizontal" | "vertical"The orientation of the element.
Machine API
The navigation menu api exposes the following methods:
valuestringThe current value of the menusetValue(value: string) => voidSets the value of the menuopenbooleanWhether the menu is openisViewportRenderedbooleanWhether the viewport is renderedgetViewportNode() => HTMLElementGets the viewport node elementorientationOrientationThe orientation of the menurepositionVoidFunctionFunction to reposition the viewport
Data Attributes
CSS Variables
Accessibility
Keyboard Interactions
- ArrowDownWhen focus is on trigger (vertical orientation), moves focus to the next trigger.
- ArrowUpWhen focus is on trigger (vertical orientation), moves focus to the previous trigger.
- ArrowRightWhen focus is on trigger (horizontal orientation), moves focus to the next trigger.
When focus is on content, moves focus to the next link. - ArrowLeftWhen focus is on trigger (horizontal orientation), moves focus to the previous trigger.
When focus is on content, moves focus to the previous link. - HomeWhen focus is on trigger, moves focus to the first trigger.
When focus is on content, moves focus to the first link. - EndWhen focus is on trigger, moves focus to the last trigger.
When focus is on content, moves focus to the last link.