Qure UI
Components

Navigation Menu

A bar or a rail of destinations, some of which open a panel of sub-destinations.

'use client'import { ActivityIcon, BrainIcon, FileTextIcon, HeartPulseIcon, ScanIcon, StethoscopeIcon } from 'lucide-react'import {  NavigationMenu, NavigationMenuContent, NavigationMenuItem, NavigationMenuItemLink,  NavigationMenuLink, NavigationMenuLinkDescription, NavigationMenuLinkText,  NavigationMenuList, NavigationMenuTrigger, NavigationMenuViewport,} from '@/registry/qure/ui/navigation-menu'export default function NavigationMenuDemo() {  return (    <NavigationMenu>      <NavigationMenuList>        <NavigationMenuItem>          <NavigationMenuItemLink href="#worklist" active>            Worklist          </NavigationMenuItemLink>        </NavigationMenuItem>        <NavigationMenuItem>          <NavigationMenuTrigger>Modalities</NavigationMenuTrigger>          <NavigationMenuContent>            <NavigationMenuLink href="#chest-xray">              <ScanIcon className="size-4" />              <NavigationMenuLinkText>                Chest X-ray                <NavigationMenuLinkDescription>                  qXR — 210 studies awaiting read                </NavigationMenuLinkDescription>              </NavigationMenuLinkText>            </NavigationMenuLink>            <NavigationMenuLink href="#head-ct">              <BrainIcon className="size-4" />              <NavigationMenuLinkText>                Head CT                <NavigationMenuLinkDescription>                  qER — intracranial haemorrhage triage                </NavigationMenuLinkDescription>              </NavigationMenuLinkText>            </NavigationMenuLink>            <NavigationMenuLink href="#lung-ct">              <ActivityIcon className="size-4" />              <NavigationMenuLinkText>                Lung CT                <NavigationMenuLinkDescription>                  qCT — nodule detection and volumetry                </NavigationMenuLinkDescription>              </NavigationMenuLinkText>            </NavigationMenuLink>          </NavigationMenuContent>        </NavigationMenuItem>        <NavigationMenuItem>          <NavigationMenuTrigger>Reporting</NavigationMenuTrigger>          <NavigationMenuContent>            <NavigationMenuLink href="#drafts">              <FileTextIcon className="size-4" />              <NavigationMenuLinkText>                Draft reports                <NavigationMenuLinkDescription>                  12 unsigned, oldest from 2 April                </NavigationMenuLinkDescription>              </NavigationMenuLinkText>            </NavigationMenuLink>            <NavigationMenuLink href="#templates">              <StethoscopeIcon className="size-4" />              <NavigationMenuLinkText>                Templates                <NavigationMenuLinkDescription>                  Structured findings by body part                </NavigationMenuLinkDescription>              </NavigationMenuLinkText>            </NavigationMenuLink>            <NavigationMenuLink href="#peer-review">              <HeartPulseIcon className="size-4" />              <NavigationMenuLinkText>                Peer review                <NavigationMenuLinkDescription>                  Discrepancies flagged in the last 30 days                </NavigationMenuLinkDescription>              </NavigationMenuLinkText>            </NavigationMenuLink>          </NavigationMenuContent>        </NavigationMenuItem>        <NavigationMenuItem>          <NavigationMenuItemLink href="#audit">Audit</NavigationMenuItemLink>        </NavigationMenuItem>      </NavigationMenuList>      <NavigationMenuViewport />    </NavigationMenu>  )}

Installation

npx shadcn@latest add https://qure-ui.qure.ai/r/navigation-menu.json

Usage

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

'use client'import {  ClipboardListIcon, FileTextIcon, LayoutGridIcon, SettingsIcon,  ShieldCheckIcon, UsersIcon,} from 'lucide-react'import {  NavigationMenu, NavigationMenuItem, NavigationMenuItemLink,  NavigationMenuLabel, NavigationMenuList,} from '@/registry/qure/ui/navigation-menu'/** * The rail from "Side navigation - large". No panels: every row goes * somewhere, so every row is a link and the whole thing is one `nav`. */export default function NavigationMenuSide() {  return (    <NavigationMenu orientation="vertical" size="lg" className="w-64" aria-label="Sections">      <NavigationMenuList>        <NavigationMenuLabel>Reading</NavigationMenuLabel>        <NavigationMenuItem>          <NavigationMenuItemLink href="#worklist">            <ClipboardListIcon className="size-4" />            Worklist          </NavigationMenuItemLink>        </NavigationMenuItem>        <NavigationMenuItem>          <NavigationMenuItemLink href="#studies" active>            <LayoutGridIcon className="size-4" />            Studies          </NavigationMenuItemLink>        </NavigationMenuItem>        <NavigationMenuItem>          <NavigationMenuItemLink href="#reports">            <FileTextIcon className="size-4" />            Reports          </NavigationMenuItemLink>        </NavigationMenuItem>        <NavigationMenuLabel>Administration</NavigationMenuLabel>        <NavigationMenuItem>          <NavigationMenuItemLink href="#users">            <UsersIcon className="size-4" />            Radiologists          </NavigationMenuItemLink>        </NavigationMenuItem>        <NavigationMenuItem>          <NavigationMenuItemLink href="#audit">            <ShieldCheckIcon className="size-4" />            Audit trail          </NavigationMenuItemLink>        </NavigationMenuItem>        <NavigationMenuItem>          <NavigationMenuItemLink href="#settings">            <SettingsIcon className="size-4" />            Settings          </NavigationMenuItemLink>        </NavigationMenuItem>      </NavigationMenuList>    </NavigationMenu>  )}

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

'use client'import {  NavigationMenu, NavigationMenuItem, NavigationMenuItemLink, NavigationMenuList,} from '@/registry/qure/ui/navigation-menu'const sizes = ['sm', 'md', 'lg'] as constexport default function NavigationMenuSize() {  return (    <div className="flex w-full flex-col gap-6">      {sizes.map(size => (        <div key={size} className="flex flex-col gap-1">          <span className="text-xs text-muted-foreground">{size}</span>          <NavigationMenu size={size} aria-label={`Study sections, ${size}`}>            <NavigationMenuList>              <NavigationMenuItem>                <NavigationMenuItemLink href="#images" active>Images</NavigationMenuItemLink>              </NavigationMenuItem>              <NavigationMenuItem>                <NavigationMenuItemLink href="#findings">Findings</NavigationMenuItemLink>              </NavigationMenuItem>              <NavigationMenuItem>                <NavigationMenuItemLink href="#priors">Priors</NavigationMenuItemLink>              </NavigationMenuItem>            </NavigationMenuList>          </NavigationMenu>        </div>      ))}    </div>  )}

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

'use client'import Link from 'next/link'import { usePathname } from 'next/navigation'import {  NavigationMenu, NavigationMenuContent, NavigationMenuItem, NavigationMenuLink,  NavigationMenuLinkText, NavigationMenuList, NavigationMenuTrigger,  NavigationMenuViewport,} from '@/registry/qure/ui/navigation-menu'const series = [  { href: '/docs/components/navigation-menu', label: 'Series 1 — Scout' },  { href: '/docs/components/breadcrumb', label: 'Series 4 — Axial 1mm' },  { href: '/docs/components/tabs', label: 'Series 7 — Coronal MPR' },]/** * `render` swaps the anchor for the router's link without losing the class, * the composite behaviour or `aria-current`. `closeOnClick` matters here and * not on a plain href: a client-side route change does not unmount the panel, * so without it you navigate and the panel stays open over the new page. */export default function NavigationMenuRouter() {  const pathname = usePathname()  return (    <NavigationMenu>      <NavigationMenuList>        <NavigationMenuItem>          <NavigationMenuTrigger>Series</NavigationMenuTrigger>          <NavigationMenuContent>            {series.map(item => (              <NavigationMenuLink                key={item.href}                active={pathname === item.href}                closeOnClick                render={<Link href={item.href} />}              >                <NavigationMenuLinkText>{item.label}</NavigationMenuLinkText>              </NavigationMenuLink>            ))}          </NavigationMenuContent>        </NavigationMenuItem>      </NavigationMenuList>      <NavigationMenuViewport />    </NavigationMenu>  )}

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

'use client'import * as React from 'react'import { Button } from '@/registry/qure/ui/button'import {  NavigationMenu, NavigationMenuContent, NavigationMenuItem, NavigationMenuLink,  NavigationMenuLinkText, NavigationMenuList, NavigationMenuTrigger,  NavigationMenuViewport,} from '@/registry/qure/ui/navigation-menu'/** * The value is the item that is open, or `null`. Useful when something outside * the bar has to close it — finishing a hanging protocol, say — or when a * walkthrough needs to open a panel for the reader. */export default function NavigationMenuControlled() {  const [value, setValue] = React.useState<string | null>(null)  return (    <div className="flex flex-col items-center gap-4">      <NavigationMenu value={value} onValueChange={setValue} delay={0}>        <NavigationMenuList>          <NavigationMenuItem value="protocols">            <NavigationMenuTrigger>Hanging protocols</NavigationMenuTrigger>            <NavigationMenuContent>              <NavigationMenuLink href="#chest-compare">                <NavigationMenuLinkText>Chest — current vs prior</NavigationMenuLinkText>              </NavigationMenuLink>              <NavigationMenuLink href="#neuro-4up">                <NavigationMenuLinkText>Neuro — 4-up</NavigationMenuLinkText>              </NavigationMenuLink>            </NavigationMenuContent>          </NavigationMenuItem>        </NavigationMenuList>        <NavigationMenuViewport />      </NavigationMenu>      <div className="flex gap-2">        <Button size="sm" variant="secondary" onClick={() => setValue('protocols')}>          Open        </Button>        <Button size="sm" variant="tertiary" onClick={() => setValue(null)}>          Close        </Button>      </div>      <p className="text-sm text-muted-foreground">        Open item: <code>{value ?? 'null'}</code>      </p>    </div>  )}

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

PropTypeDefaultDescription
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.
valuestring | nullControlled: which item's panel is open.
onValueChange(value: string | null) => voidFires when the open item changes.
delaynumberHover 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.

PropTypeDefaultDescription
side"top" | "right" | "bottom" | "left""bottom"Which side of the bar the panel opens on.
sideOffsetnumber8Gap from the bar.
align"start" | "center" | "end""center"Alignment along that side.
collisionPaddingnumber16Minimum gap from the viewport edge.

Both forward to Base UI's NavigationMenu.Link: active, closeOnClick, render, href. See the Base UI documentation for the rest.

On this page