Photon Design System

Navigation Menu

Primary site navigation — a horizontal bar whose items open dropdown panels of grouped links that animate and resize to their content. Built on Base UI NavigationMenu. For application action menus use Menu.

Import

import { NavigationMenu } from "@photon-ai/pho-ui/components/navigation-menu";

Basic

Each NavigationMenu.Item pairs a Trigger with Content; the content renders into a shared Viewport inside the popup. A plain NavigationMenu.Link makes a top-level item with no dropdown.

<NavigationMenu.Root>
  <NavigationMenu.List>
    <NavigationMenu.Item>
      <NavigationMenu.Trigger>
        Products
        <NavigationMenu.Icon>
          <CaretDown />
        </NavigationMenu.Icon>
      </NavigationMenu.Trigger>
      <NavigationMenu.Content>
        <NavigationMenu.Link href="/deploy">Deploy</NavigationMenu.Link>
      </NavigationMenu.Content>
    </NavigationMenu.Item>
  </NavigationMenu.List>
  <NavigationMenu.Portal>
    <NavigationMenu.Positioner>
      <NavigationMenu.Popup>
        <NavigationMenu.Viewport />
      </NavigationMenu.Popup>
    </NavigationMenu.Positioner>
  </NavigationMenu.Portal>
</NavigationMenu.Root>

Anatomy

  • NavigationMenu.Root / .List / .Item — the <nav> bar (<ul> / <li>) and its items
  • NavigationMenu.Trigger + .Content — a dropdown item’s <button> and the panel it opens
  • NavigationMenu.Icon — the caret inside a trigger; rotates 180° while the panel is open
  • NavigationMenu.Link — an <a> link (top-level, or inside a panel’s content)
  • NavigationMenu.Portal → Positioner → Popup → Viewport — the shared, resizing panel

Props

Every part also accepts className (a plain string) and render — Base UI’s prop for swapping the element it renders (e.g. a framework <Link> for a NavigationMenu.Link). The rest forward to the underlying Base UI part.

NavigationMenu.Root (<nav>; holds open state)

  • value — any — controlled value of the open item; non-nullish keeps the menu open
  • defaultValue — any (default null) — initially open item when uncontrolled
  • onValueChange — (value, details) => void — fires when the open item changes
  • delay — number (default 50) — hover-open delay in ms
  • closeDelay — number (default 50) — hover-close delay in ms
  • orientation — horizontal · vertical (default horizontal) — arrow-key navigation axis

NavigationMenu.Trigger (<button>) — opens its Content; gets aria-expanded and data-popup-open (which drives the Icon rotation).

NavigationMenu.Content (<div>) — the body for one item, mounted into the shared Viewport.

NavigationMenu.Link (<a>) — forwards href and any anchor attribute; pass render to route through your framework’s link.

NavigationMenu.Positioner (<div>) — positions the popup against the active trigger

  • side — top · right · bottom · left (default bottom) — which side of the trigger to place the popup
  • sideOffset — number (default 8) — gap between trigger and popup (pho-ui default; Base UI’s own default is 0)
  • align — start · center · end (default center) — alignment along that side
  • alignOffset — number (default 0) — shift along the alignment axis
  • anchor — Element · ref · () => Element — element to position against; defaults to the active trigger

NavigationMenu.Popup (<nav>) — the floating surface; sized to --popup-width / --popup-height and animated via data-starting-style / data-ending-style.

NavigationMenu.Viewport (<div>) — the single window each item’s Content renders into; the popup resizes to fit it.

Accessibility

  • Semantics — Root renders a <nav> landmark (and Popup another), so give the Root an aria-label when a page has more than one nav. Each Trigger is a <button> carrying aria-expanded for its panel (mirrored by data-popup-open), and Link renders a real <a> — reachable, focusable, and openable in a new tab like any link.
  • Keyboard
    • Tab / Shift+Tab move across the top-level triggers and links, then into an open panel and onward through the page
    • Arrow Left / Arrow Right move focus between adjacent triggers (Arrow Up / Arrow Down when orientation="vertical")
    • Enter / Space on a focused trigger open or close its panel
    • Escape closes the open panel and returns focus to its trigger
  • Focus — triggers and links show the brand focus ring on keyboard focus only (focus-visible). The panel is a disclosure, not a focus trap: tabbing past its last link simply moves on in the page and closes it.

Best practices

  • Use a navigation menu for a website’s primary nav with grouped destinations; keep the top level to a handful of items.
  • Put the most important links first in each panel and give them a short description so the dropdown earns its space.
  • For deep, app-style command surfaces use a Menubar or Menu instead — a navigation menu is about going places, not running commands.