Navigation Menu
A bar or a rail of destinations, some of which open a panel of sub-destinations.
Installation
npx shadcn@latest add https://qure-ui.qure.ai/r/navigation-menu.jsonUsage
import {
NavigationMenu, NavigationMenuContent, NavigationMenuItem,
NavigationMenuItemLink, NavigationMenuLink, NavigationMenuLinkText,
NavigationMenuList, NavigationMenuTrigger, NavigationMenuViewport,
} from '@/components/ui/navigation-menu'<NavigationMenu>
<NavigationMenuList>
<NavigationMenuItem>
<NavigationMenuItemLink href="/worklist" active>Worklist</NavigationMenuItemLink>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuTrigger>Modalities</NavigationMenuTrigger>
<NavigationMenuContent>
<NavigationMenuLink href="/chest-xray">
<NavigationMenuLinkText>Chest X-ray</NavigationMenuLinkText>
</NavigationMenuLink>
</NavigationMenuContent>
</NavigationMenuItem>
</NavigationMenuList>
<NavigationMenuViewport />
</NavigationMenu>This is not a menu of commands — that is Menubar or Dropdown Menu. The test is what the rows do: if they take you somewhere they belong here and should be links; if they act on what you are looking at, they belong in a menu.
The two shapes come from the design system's own shells — horizontal is "Top navigation", vertical
is "Side navigation" — and size picks the step those are drawn at.
Composition
<NavigationMenu> {/* nav; owns orientation and size */}
<NavigationMenuList> {/* ul */}
<NavigationMenuLabel> {/* a section heading in a rail */}
<NavigationMenuItem> {/* li */}
<NavigationMenuItemLink /> {/* a destination with no panel */}
<NavigationMenuTrigger> {/* opens a panel */}
<NavigationMenuContent> {/* what the panel holds */}
<NavigationMenuLink> {/* one row inside a panel */}
<NavigationMenuLinkText>
<NavigationMenuLinkDescription>
<NavigationMenuViewport /> {/* the one shared panel — sibling of the list */}
</NavigationMenu>NavigationMenuContent is written inside its item but moved into the shared popup when that
item becomes active. That is what lets one popup slide and resize between two panels instead of
closing and reopening.
NavigationMenuViewport must be rendered exactly once, as a sibling of the list rather than a
child of an item. Leave it out and no panel appears at all — the triggers still open, they just
have nowhere to open into.
Note the two different link components. NavigationMenuItemLink is a destination sitting in the
bar itself; NavigationMenuLink is a row inside a panel. They are the same primitive with
different styling, kept separate so a panel row and a bar row cannot be confused at a glance.
As a side rail
orientation="vertical" with size="lg". No panels here: every row goes somewhere, so every row
is a link and the whole thing is a single nav.
NavigationMenuLabel is the section heading — "Reading", "Administration". It renders an <li>
with role="presentation", because it labels the rows under it and is not a row you can land on.
Give a second navigation region its own aria-label. A page with a top bar and a side rail has
two nav landmarks, and "navigation, navigation" is not a useful choice to offer someone
listening.
Size
md is the default. sm suits a secondary bar beneath a primary one; lg matches the side rail
in the design system.
With a router
render swaps the anchor for the router's link without losing the class, the composite behaviour
or aria-current:
<NavigationMenuLink active={pathname === href} closeOnClick render={<Link href={href} />}>closeOnClick matters here and does not on a plain href. A full page load unmounts everything;
a client-side route change does not, so without it you navigate and the panel stays open over the
new page.
active puts data-active on the element and aria-current="page" in the accessibility tree —
so the current page is marked for someone who cannot see which row is tinted.
Controlled
Open item: null
The value is the item that is open, or null. Useful when something outside the bar has to close
it, or when a walkthrough needs to open a panel for the reader.
API Reference
NavigationMenu
| Prop | Type | Default | Description |
|---|---|---|---|
size | "sm" | "md" | "lg" | "md" | The step the bar or rail is drawn at. |
orientation | "horizontal" | "vertical" | "horizontal" | Bar or rail. Also sets the arrow keys. |
value | string | null | — | Controlled: which item's panel is open. |
onValueChange | (value: string | null) => void | — | Fires when the open item changes. |
delay | number | — | Hover delay before a panel opens. |
orientation is written to data-orientation by this component rather than by Base UI. The
primitive takes the prop and uses it for the arrow keys, but its state is only { open, nested },
so nothing lands on the element and CSS cannot see it.
NavigationMenuViewport
| Prop | Type | Default | Description |
|---|---|---|---|
side | "top" | "right" | "bottom" | "left" | "bottom" | Which side of the bar the panel opens on. |
sideOffset | number | 8 | Gap from the bar. |
align | "start" | "center" | "end" | "center" | Alignment along that side. |
collisionPadding | number | 16 | Minimum gap from the viewport edge. |
NavigationMenuLink and NavigationMenuItemLink
Both forward to Base UI's NavigationMenu.Link: active, closeOnClick, render, href. See
the Base UI documentation for the rest.