Navigation Menu
A collection of links for navigating websites — triggers that open one shared panel, plain links styled to match, and any routing library's link through render.
- Status
- Since
0.3.0- Accessibility pattern
- APG disclosure navigation
Last updated
Install
Configure registry credentials first using the Quickstart. Before copying this item or any transitive registry dependency, verify the signed manifest and save the exact bytes for Navigation Menu before installation. Retain the digest printed by this command.
pnpm exec vegastack-design verify navigation-menuInstall with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.
pnpm dlx shadcn@4.21.0 add @vegastack/navigation-menuRun the exact offline --post-write command printed by the preflight, using its saved item path and independently retained --expected-integrity digest. Use the saved preflight for every transitive registry dependency too; stop on any mismatch before using the components.
Usage
import {
NavigationMenu,
NavigationMenuContent,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
NavigationMenuTrigger,
} from "@/components/ui/navigation-menu";
<NavigationMenu>
<NavigationMenuList>
<NavigationMenuItem>
<NavigationMenuTrigger>Item One</NavigationMenuTrigger>
<NavigationMenuContent>
<NavigationMenuLink>Link</NavigationMenuLink>
</NavigationMenuContent>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>;A navigation menu is a desktop row. Its list does not wrap, and its panel is as wide as its content,
clamped to the space beside the screen edge with anything wider clipped. Fitting a phone is the
call site's job: size each panel responsively (w-64 sm:w-96, not a fixed w-96), hide secondary
triggers below a breakpoint (hidden sm:flex), and below sm move the navigation into a Sheet.
The demo above does the first two.
Anatomy
Examples
Composition
NavigationMenu renders the portal, the positioner, the popup and the viewport itself, so the call
site is the nav row alone: a list of items, each holding a trigger and its content, or a plain link.
align on NavigationMenu decides which edge of the row the shared panel hangs from.
NavigationMenu
├── NavigationMenuList
│ ├── NavigationMenuItem
│ │ ├── NavigationMenuTrigger
│ │ │ └── NavigationMenuIndicator
│ │ └── NavigationMenuContent
│ │ ├── NavigationMenuLink
│ │ └── NavigationMenuLink
│ └── NavigationMenuItem
│ └── NavigationMenuLink
└── (portal · positioner · popup · viewport — rendered by NavigationMenu)Link Component
render on NavigationMenuLink replaces the element it renders, which is how a routing library's
link gets the menu's styling, its data-slot and its keyboard behaviour. In a Next.js app that is
render={<Link href="/docs" />}. Adding navigationMenuTriggerStyle() makes a top-level link sit
flush with the triggers beside it.
RTL
Wrap the subtree in DirectionProvider direction="rtl" so Base UI resolves the logical sides, set
align="end" so the panel hangs from the right edge of the nav row, and put dir on
NavigationMenuContent — the panel portals out of the dir="rtl" subtree, so the attribute has to
travel with it.
API Reference
NavigationMenu
NavigationMenu adds no props of its own — it accepts everything the underlying element or Base UI primitive accepts (className, ref, ARIA attributes, event handlers).
NavigationMenuList
NavigationMenuList adds no props of its own — it accepts everything the underlying element or Base UI primitive accepts (className, ref, ARIA attributes, event handlers).
NavigationMenuItem
NavigationMenuItem adds no props of its own — it accepts everything the underlying element or Base UI primitive accepts (className, ref, ARIA attributes, event handlers).
NavigationMenuTrigger
NavigationMenuTrigger adds no props of its own — it accepts everything the underlying element or Base UI primitive accepts (className, ref, ARIA attributes, event handlers).
NavigationMenuContent
NavigationMenuContent adds no props of its own — it accepts everything the underlying element or Base UI primitive accepts (className, ref, ARIA attributes, event handlers).
NavigationMenuLink
NavigationMenuLink adds no props of its own — it accepts everything the underlying element or Base UI primitive accepts (className, ref, ARIA attributes, event handlers).
NavigationMenuIndicator
NavigationMenuIndicator adds no props of its own — it accepts everything the underlying element or Base UI primitive accepts (className, ref, ARIA attributes, event handlers).
NavigationMenuPositioner
NavigationMenuPositioner adds no props of its own — it accepts everything the underlying element or Base UI primitive accepts (className, ref, ARIA attributes, event handlers).
navigationMenuTriggerStyle() is the trigger's class recipe, exported so a plain
NavigationMenuLink in the nav row can wear it.
Accessibility
- Renders Base UI's Navigation Menu: the row is a
nav, each trigger is a button witharia-expandedandaria-controls, and the panel is a labelled region. Esc closes the open panel and returns focus to its trigger. - Arrow keys move between the triggers in the row and into the open panel, Home and End jump to the ends, and Tab leaves the menu entirely — the row is one tab stop, not one per link.
- A disabled trigger keeps its pointer events (FRM-4), so a tooltip can explain why it is
unavailable; it is still
aria-disabledand still cannot be activated. - The panel portals out of the nav's subtree, so
NavigationMenuPositionerre-applies the theme scope on the positioner inside the portal (OVL-13) — a dark or scoped surface survives the portal. - Focus is the global 2px
:focus-visibleoutline frombase.css. Upstream suppressed the outline for every link inside the panel; that suppression is removed here (FOC-1, FOC-6), because the panel is where this menu's keyboard navigation actually lives.
| Contract | States tested |
|---|---|
| Behaviour | default, open, closed, active, disabled |
| Accessibility | native-or-base-ui-semantics, browser-accessibility-test, keyboard-navigation, disabled |
| Visual | default, hover, open, active, disabled, focus-visible |
Do / Don't
Deviations
Upstream's file plus packages/ui/upstream/patches/navigation-menu.patch. Every hunk:
- FOC-1, FOC-6 —
outline-none focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:outline-1goes fromnavigationMenuTriggerStyleand fromNavigationMenuLink, and the pair**:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-nonegoes fromNavigationMenuContent— it suppressed the outline for every link inside the popup, which is where this menu's keyboard navigation actually lives. - FRM-4 —
disabled:pointer-events-noneis dropped from the trigger recipe; the opacity stays. - OVL-13 —
NavigationMenuPositionerreadsuseInternalThemeScope()and attaches it to the Positioner INSIDENavigationMenuPrimitive.Portal— the same host shapepopover,select,comboboxandtooltipcarry.tooling/verify-portal-theme-scope.mjsinventories this host. - API-16 — the file gains
'use client'. Upstream ships none because it calls no hook of its own; OVL-13'suseInternalThemeScope()is a hook, and this file is the lowest leaf that needs the boundary. - BRD-1 — The shared viewport popup and the viewport-less (
viewport={false})NavigationMenuContentdraw a real 1pxborder border-borderinstead of upstream'sring-1 ring-foreground/10box-shadow outline. A border survives forced colours and matches Elevation; nothing else about the surface moves. - DOC-2 —
import { cn } from "cn"becomes@vegastack/design; our npm layer ownscn. - DOC-1 — prettier reflow plus the
registry-stamp.mjsprovenance header the three-copy model needs; no decision of its own.