bx

Navigation Menu

A collection of links for navigating websites.

tsx
<NavigationMenu ariaLabel="Documentation">
  <NavigationMenuList>
    <NavigationMenuItem>
      <NavigationMenuTrigger>Getting started</NavigationMenuTrigger>
      <NavigationMenuContent>
        <NavigationMenuLink href="#">Introduction</NavigationMenuLink>
        <NavigationMenuLink href="#">Installation</NavigationMenuLink>
        <NavigationMenuLink href="#">Typography</NavigationMenuLink>
      </NavigationMenuContent>
    </NavigationMenuItem>
    <NavigationMenuItem>
      <NavigationMenuTrigger>Components</NavigationMenuTrigger>
      <NavigationMenuContent>
        <NavigationMenuLink href="#">Alert Dialog</NavigationMenuLink>
        <NavigationMenuLink href="#">Hover Card</NavigationMenuLink>
        <NavigationMenuLink href="#">Progress</NavigationMenuLink>
      </NavigationMenuContent>
    </NavigationMenuItem>
  </NavigationMenuList>
</NavigationMenu>

NavigationMenu is a horizontal navigation bar with sub-panels that open on hover, click or keyboard (the public site navigation pattern, as in the shadcn.com header). Each top-level item opens a wider panel with hierarchical links or a section preview.

Each NavigationMenuItem carries the named group class group/nav-item and its NavigationMenuContent is revealed with group-hover/nav-item:, so hover works without JavaScript. The island adds the rest: click, Enter, Space and ArrowDown open the panel, Escape and a focus move close it, and aria-expanded follows. The panel opens under its own item; there is no shared animated viewport as in the Radix version.

Installation

Terminal
$ bext ui add navigation-menu

Usage

tsx
import { NavigationMenu, NavigationMenuList, NavigationMenuItem, NavigationMenuTrigger, NavigationMenuContent, NavigationMenuLink } from "@bext-stack/ui/primitives"

<NavigationMenu>
  <NavigationMenuList>
    <NavigationMenuItem>
      <NavigationMenuTrigger>Getting started</NavigationMenuTrigger>
      <NavigationMenuContent>
        <NavigationMenuLink href="/docs">Introduction</NavigationMenuLink>
        <NavigationMenuLink href="/docs/installation">Installation</NavigationMenuLink>
      </NavigationMenuContent>
    </NavigationMenuItem>
  </NavigationMenuList>
</NavigationMenu>

Anatomy

<NavigationMenu>
  └── <NavigationMenuList>
      ├── <NavigationMenuItem>                       // group/nav-item
      │   ├── <NavigationMenuTrigger>Getting started</NavigationMenuTrigger>
      │   └── <NavigationMenuContent>                // revealed on hover or data-state="open"
      │       └── <NavigationMenuLink href="/docs">Introduction</NavigationMenuLink>
      └── <NavigationMenuItem>...</NavigationMenuItem>

API Reference

Prop Type Default Description
ariaLabel string "Main" Accessible name of the <nav> landmark.
viewport boolean true Only sets data-viewport on the <nav>, for shadcn API parity. No viewport element is rendered: panels open under their own item.
className string — Extra classes on the <nav>.
Prop Type Default Description
className string — flex items-center gap-1.
Prop Type Default Description
className string — Relative group/nav-item wrapper.
Prop Type Default Description
controls string — id of the panel, written to aria-controls. When omitted, the island generates an id on the sibling NavigationMenuContent and links it.
className string — Button h-9 px-4, with a chevron-down that rotates while the panel is open.
Prop Type Default Description
id string — id of the panel, for the trigger's controls prop.
className string — Panel invisible → visible when the item is hovered or the panel is open. min-w-[220px], rounded-md border bg-popover.
Prop Type Default Description
href string "#" URL.
active boolean false Sets data-active="true", which applies bg-accent.
Prop Type Default Description
className string — Kept for shadcn API parity; neither is wired to the panels. NavigationMenuViewport renders nothing unless it is given children. NavigationMenuIndicator renders a static styled element.

Data Attributes

Attributes you can target in custom CSS to override the style for each state.

Attribute On Values Description
data-slot every sub-component "navigation-menu" | "navigation-menu-list" | "navigation-menu-item" | "navigation-menu-trigger" | "navigation-menu-content" | "navigation-menu-link" | "navigation-menu-viewport" | "navigation-menu-indicator" Stable selector for custom styling.
aria-expanded NavigationMenuTrigger "true" | "false" Whether the panel of this item is open. Toggled by the island.
data-state NavigationMenuTrigger, NavigationMenuContent "open" | "closed" (absent before the first interaction) Set by the island. "open" makes the panel visible and rotates the chevron.
data-viewport NavigationMenu "true" | "false" Reflects the viewport prop.
data-active NavigationMenuLink "true" (absent otherwise) Present when active is set. Applies bg-accent.

Keyboard

Key Action
Tab Focuses the triggers one by one. With a panel open, Tab goes from the trigger into its links. Leaving the item closes the panel.
Enter / Space On a trigger: opens or closes its panel. The focus stays on the trigger.
Arrow Down On a trigger: opens the panel and focuses its first link. On a link: next link.
Arrow Up On a link: previous link, or the trigger from the first link.
Escape Closes the open panel and returns the focus to its trigger.

Accessibility

Each trigger is a <button> with aria-expanded and aria-controls (the disclosure pattern, not role="menu"). Its panel opens on hover, click, tap, Enter, Space and ArrowDown. It closes on Escape (the focus returns to the trigger), on a click outside, when the focus leaves the item and when another panel opens. Only one panel is open at a time.

A closed panel is visibility: hidden, so its links are out of the tab order and hidden from assistive technologies; once open they follow the trigger in the tab order. Without JavaScript only the hover reveal works, so keep every destination reachable another way if that case matters to you.

NavigationMenu renders a <nav> named "Main". Pass ariaLabel to rename it when the page has several navigation landmarks.

Try it live
Open in play

Edit this component live in the bext playground. The PRISM + signals version is the bext-native idiom: fine-grained reactivity, no virtual DOM.

src/app/page.tsx 2 files · signals · runs in your browser
Pure PRISM + real bext signals: fine-grained, no re-render.