Navigation Menu
A collection of links for navigating websites.
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
$ bext ui add navigation-menu
Copy and paste the following into src/components/ui/navigation-menu.tsx.
This component is interactive. Make sure the shared island is loaded (see Islands). It reacts to data-ui="navigation-menu" hooks.
Usage
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
NavigationMenu
| 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>. |
NavigationMenuList
| Prop | Type | Default | Description |
|---|---|---|---|
| className | string | — | flex items-center gap-1. |
NavigationMenuItem
| Prop | Type | Default | Description |
|---|---|---|---|
| className | string | — | Relative group/nav-item wrapper. |
NavigationMenuTrigger
| 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. |
NavigationMenuContent
| 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. |
NavigationMenuLink
| Prop | Type | Default | Description |
|---|---|---|---|
| href | string | "#" | URL. |
| active | boolean | false | Sets data-active="true", which applies bg-accent. |
NavigationMenuViewport / NavigationMenuIndicator
| 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.
Edit this component live in the bext playground. The PRISM + signals version is the bext-native idiom: fine-grained reactivity, no virtual DOM.
On This Page