Navigation Menu
Primitive

Navigation Menu

Headless navigation disclosure primitives with trigger-driven panels, indicator geometry, and a shared viewport.

Live behavior

Navigation Menu in motion

Interactive
Atom behavior · App-owned appearance
Preparing behavior…

Open a destination group and move through its links.

waiting for input

When to Use

Use NavigationMenu for website navigation where a top-level destination can open a panel of related links. Use NavList for a simple visible list of links, Menubar for application commands such as File and Edit, and Menu for a temporary action list. NavigationMenu keeps normal link and Tab behavior; it is not a menu-role widget.

Features

The behavior Atom owns before your product adds appearance.

  • Supports root and nested navigation menu scopes.
  • Supports mouse-hover delay, click/tap, and keyboard open. Touch/pen never start hover timers.
  • Provides a shared viewport that adapts to the active content size.
  • Centers horizontal viewport content on the active trigger and shifts it back inside the visible browser viewport when it would collide with an edge.
  • Provides indicator geometry CSS variables for styling arrows or active markers.
  • Supports active links and aria-current.
  • Supports horizontal and vertical orientation.

Import

tsx
import { NavigationMenu } from "@flowstack-ui/atom";

Anatomy

tsx
<NavigationMenu.Root>
  <NavigationMenu.List>
    <NavigationMenu.Item>
      <NavigationMenu.Trigger />
      <NavigationMenu.Content />
    </NavigationMenu.Item>
    <NavigationMenu.Item>
      <NavigationMenu.Link />
    </NavigationMenu.Item>
  </NavigationMenu.List>
  <NavigationMenu.Indicator />
  <NavigationMenu.Viewport />
  <NavigationMenu.Sub>
    <NavigationMenu.List>
      <NavigationMenu.Item>
        <NavigationMenu.Trigger />
        <NavigationMenu.Content />
      </NavigationMenu.Item>
    </NavigationMenu.List>
    <NavigationMenu.Viewport />
  </NavigationMenu.Sub>
</NavigationMenu.Root>

API Reference

Each part that renders DOM emits a default [data-slot]. Pass data-slot to override that value for app-specific styling or test selectors.

Root

Renders the nav landmark, owns the active panel, and coordinates opening delays, direction, orientation, and top-level keyboard navigation.

PropTypeDefault
childrenReactNoderequired
valuestring | null-
defaultValuestring-
onValueChange(value: string | null) => void-
delayDurationnumber200
skipDelayDurationnumber300
loopbooleantrue
orientation"horizontal" | "vertical""horizontal"
dir"ltr" | "rtl"Direction.Provider
asChildbooleanfalse
renderRenderProp-
ARIA attributeValues
aria-label"Main" by default; native aria-label overrides it
Data attributeValues
[data-slot]"navigation-menu"
[data-orientation]"horizontal" | "vertical"

List

Renders the item list.

PropTypeDefault
childrenReactNoderequired
asChildbooleanfalse
renderRenderProp-
Data attributeValues
[data-slot]"navigation-menu-list"
[data-orientation]"horizontal" | "vertical"

Item

Provides a value scope for a trigger/content pair or link.

PropTypeDefault
childrenReactNoderequired
valuestringrequired
asChildbooleanfalse
renderRenderProp-
Data attributeValues
[data-slot]"navigation-menu-item"

Trigger

Renders the button that controls its item's panel and participates in roving top-level keyboard navigation.

PropTypeDefault
childrenReactNoderequired
disabledbooleanfalse
asChildbooleanfalse
renderRenderProp-
ARIA attributeValues
aria-expandedWhether this item's content is open
aria-controlsGenerated content ID
aria-disabledPresent when disabled
Data attributeValues
[data-slot]"navigation-menu-trigger"
[data-state]"open" | "closed"
[data-disabled]Present when disabled

Content

Registers panel content for the shared viewport.

Content does not render at its declaration site. Its asChild and render customize the content wrapper rendered by Viewport.

PropTypeDefault
childrenReactNoderequired
asChildbooleanfalse
loopbooleanRoot value
renderRenderProp-
Data attributeValues
[data-slot]"navigation-menu-content"
[data-state]"open"
[data-motion]"from-start" | "from-end"

Renders a navigation link.

PropTypeDefault
childrenReactNoderequired
activebooleanfalse
hrefstring-
onSelect() => void-
asChildbooleanfalse
renderRenderProp-
ARIA attributeValues
aria-current"page" when active
Data attributeValues
[data-slot]"navigation-menu-link"
[data-active]Present when active

Indicator

Renders an optional active trigger indicator.

PropTypeDefault
childrenReactNode-
forceMountbooleanfalse
collisionPaddingnumber8
asChildbooleanfalse
renderRenderProp-
ARIA attributeValues
aria-hiddentrue
Data attributeValues
[data-slot]"navigation-menu-indicator"
[data-state]"visible" | "hidden"
[data-orientation]"horizontal" | "vertical"
CSS variableDescription
--atom-navigation-menu-trigger-leftActive trigger left offset
--atom-navigation-menu-trigger-topActive trigger top offset
--atom-navigation-menu-trigger-widthActive trigger width
--atom-navigation-menu-trigger-heightActive trigger height
--atom-navigation-menu-trigger-center-xActive trigger horizontal center
--atom-navigation-menu-trigger-center-yActive trigger vertical center

Viewport

Renders the active content panel.

PropTypeDefault
childrenReactNode-
forceMountbooleanfalse
asChildbooleanfalse
renderRenderProp-
Data attributeValues
[data-slot]"navigation-menu-viewport"
[data-state]"open" | "closed"
[data-orientation]"horizontal" | "vertical"
CSS variableDescription
--atom-navigation-menu-viewport-widthActive content width
--atom-navigation-menu-viewport-heightActive content height
--atom-navigation-menu-viewport-leftCollision-resolved Root-relative left offset
--atom-navigation-menu-viewport-available-widthWidth available inside the visible viewport and collision padding
--atom-navigation-menu-trigger-leftActive trigger left offset
--atom-navigation-menu-trigger-topActive trigger top offset
--atom-navigation-menu-trigger-widthActive trigger width
--atom-navigation-menu-trigger-heightActive trigger height
--atom-navigation-menu-trigger-center-xActive trigger horizontal center
--atom-navigation-menu-trigger-center-yActive trigger vertical center

Sub

Creates a nested navigation menu scope.

PropTypeDefault
childrenReactNoderequired
valuestring | null-
defaultValuestring-
onValueChange(value: string | null) => void-
orientation"horizontal" | "vertical"Parent menu orientation
asChildbooleanfalse
renderRenderProp-
Data attributeValues
[data-slot]"navigation-menu-sub"
[data-orientation]"horizontal" | "vertical"

Advanced compound components can use useNavigationMenuContext and useNavigationMenuItemContext; the matching providers and context value types are also public exports.

Examples

Shared Viewport

tsx
import { NavigationMenu } from "@flowstack-ui/atom";

export function PrimaryNavigation() {
  return (
    <NavigationMenu.Root aria-label="Primary navigation">
      <NavigationMenu.List>
        <NavigationMenu.Item value="products">
          <NavigationMenu.Trigger>Products</NavigationMenu.Trigger>
          <NavigationMenu.Content>
            <NavigationMenu.Link href="/products/analytics">
              Analytics
            </NavigationMenu.Link>
            <NavigationMenu.Link href="/products/reports">
              Reports
            </NavigationMenu.Link>
          </NavigationMenu.Content>
        </NavigationMenu.Item>
      </NavigationMenu.List>
      <NavigationMenu.Indicator />
      <NavigationMenu.Viewport />
    </NavigationMenu.Root>
  );
}
tsx
import { NavigationMenu } from "@flowstack-ui/atom";

export function DocumentationNavigation() {
  return (
    <NavigationMenu.Root aria-label="Documentation">
      <NavigationMenu.List>
        <NavigationMenu.Item value="docs">
          <NavigationMenu.Link href="/docs" active>
            Docs
          </NavigationMenu.Link>
        </NavigationMenu.Item>
      </NavigationMenu.List>
    </NavigationMenu.Root>
  );
}

The package also exports getNavigationMenuGeometry, getNavigationMenuGeometryStyle, and getNavigationMenuViewportSizeStyle for consumers that need the same indicator or viewport measurements outside the default parts.

Accessibility

Root renders a nav landmark with an accessible name. Triggers expose expanded state and controlled content IDs. Links use native anchor semantics and aria-current="page" when active. Text direction can be set with dir on Root or inherited from Direction.Provider.

Pointer hover timing is restricted to mouse input. Touch and pen use the same click disclosure path as other directly activated controls. Responsive replacement with a Drawer is an application/styled-layer decision; Atom does not change this native disclosure navigation into command-menu semantics.

NavigationMenu follows the WAI-ARIA APG disclosure navigation example for site navigation. It does not use menu, menubar, or menuitem roles, and it does not trap focus. Tab and Shift+Tab remain the primary way to move through visible buttons and links. Arrow keys supplement normal tab navigation.

Trigger Keys

KeyDescription
Enter / SpaceOpens or closes a trigger and keeps focus on the trigger
ArrowDown / ArrowUpIn horizontal orientation, opens content and moves focus to the first or last focusable content element
ArrowRight / ArrowLeftIn horizontal orientation, moves between top-level controls, mirrored in RTL
ArrowDown / ArrowUpIn vertical orientation, moves between top-level controls
ArrowRightIn vertical LTR, opens content and moves focus to the first focusable content element
ArrowLeftIn vertical RTL, opens content and moves focus to the first focusable content element
Home / EndMoves to the first or last top-level control
EscapeCloses the active panel and restores focus to its trigger

Top-level arrow navigation includes both disclosure triggers and direct top-level links. When a panel is open, moving to another trigger switches the open panel. Moving to a direct link closes the open panel without activating the link.

Content Keys

KeyDescription
Tab / Shift+TabMoves through visible focusable content and then returns to the top-level navigation order
ArrowDown / ArrowUpMoves to the next or previous focusable content element in DOM order
Home / EndMoves to the first or last focusable content element
EscapeCloses the active panel and restores focus to its trigger

Content arrow navigation uses Root loop by default. Set loop={false} on Content to stop ArrowUp and ArrowDown at the first or last focusable content element without changing top-level Trigger/Link wrapping.

When focus leaves the navigation region, the active panel closes. Nested navigation menu scopes close from the inside out: Escape first closes the innermost sub menu and restores focus to its trigger; a later Escape can close the parent panel.

Changelog

0.21.0

  • Added collision-aware, active-trigger-centered horizontal Viewport geometry. Viewport now exposes its resolved Root-relative inline position and available visible width, follows visual-viewport resize and zoom movement, and accepts collisionPadding without requiring a styled layer to remeasure the panel.

0.20.12

  • Added public Agent Knowledge for component selection, required composition, recurring mistakes, and validation.

0.20.11

  • Kept a newly hover-opened Trigger open through the click generated by the same pointer activation, so switching directly between open items does not close the destination Trigger in Firefox or WebKit.

0.20.10

  • Exposed the active trigger's measured geometry variables directly on NavigationMenu.Viewport, allowing styled vertical menus to keep a shared Viewport aligned with later triggers without repeating Atom measurement. Geometry remains Root-relative when consumers place Viewport inside an authored positioning wrapper.

0.12.0

  • Restricted delayed hover open/close timers to mouse input so touch and pen use only the existing click/tap disclosure path.
  • Requalified native navigation semantics, orientation, RTL, direct links, viewport geometry, indicator geometry, nested scopes, and focus-out close.

0.2.0

  • Fixed NavigationMenu.Indicator and NavigationMenu.Sub asChild rendering so slotted children keep their own contents.
  • Split NavigationMenu looping so Root controls top-level Trigger/Link wrapping and Content can override content arrow-key wrapping.
  • Completed disclosure-navigation keyboard behavior for NavigationMenu, including direct top-level links, vertical orientation, content arrow navigation, focus-out closing, and nested Escape focus restoration.
  • Added loop to control content arrow-key wrapping and made content arrows follow focusable DOM order.
  • Added standard asChild/render customization support to the remaining NavigationMenu parts, including viewport-rendered content.
  • Fixed data-slot override support across NavigationMenu parts, including viewport-rendered content.
  • Added horizontal trigger roving keyboard navigation for NavigationMenu, including RTL-mirrored ArrowLeft and ArrowRight handling.
  • Added Direction.Provider fallback for NavigationMenu.Root direction.
  • Added shared dismissable layer Escape handling so NavigationMenu panels close as the topmost active layer when nested with other overlays.
  • Refined trigger, indicator, and viewport callback dependencies to avoid recreating callbacks from the full context object.

0.1.0

  • Initial Atom release.