Skip to content

Navigation Menu

Top-level site navigation with rich, animated panels, for marketing sites and documentation where a section needs more than a link.

SourceBase UIRegistry
import Link from "next/link"

import {
  NavigationMenu,
  NavigationMenuContent,
  NavigationMenuItem,
  NavigationMenuLink,
  NavigationMenuList,
  NavigationMenuTrigger,
  navigationMenuTriggerStyle,
} from "@/components/ui/navigation-menu"

const components = [
  {
    title: "Button",
    href: "/docs/components/button",
    description: "Triggers an action. The most important interactive element.",
  },
  {
    title: "Dialog",
    href: "/docs/components/dialog",
    description: "A focused task on top of the page, with a clear way out.",
  },
  {
    title: "Tabs",
    href: "/docs/components/tabs",
    description: "Switch between related views without leaving the page.",
  },
  {
    title: "Command",
    href: "/docs/components/command",
    description: "Fast, keyboard-first search and actions.",
  },
  {
    title: "Select",
    href: "/docs/components/select",
    description: "Choose one option from a list of predefined values.",
  },
  {
    title: "Toast",
    href: "/docs/components/toast",
    description: "Brief, non-blocking feedback about an operation.",
  },
]

export default function NavigationMenuDemo() {
  return (
    <NavigationMenu>
      <NavigationMenuList>
        <NavigationMenuItem>
          <NavigationMenuTrigger>Getting started</NavigationMenuTrigger>
          <NavigationMenuContent>
            <ul className="grid w-120 grid-cols-[11rem_1fr] gap-1.5">
              <li className="row-span-3">
                <NavigationMenuLink
                  render={<Link href="/docs" />}
                  className="flex h-full flex-col items-start justify-end gap-1.5 rounded-lg bg-muted p-4 transition-colors duration-fast hover:bg-gray-4"
                >
                  <span className="text-heading-sm text-foreground">prfct</span>
                  <span className="text-body-xs text-muted-foreground">
                    Interfaces, perfected.
                  </span>
                </NavigationMenuLink>
              </li>
              {[
                {
                  title: "Introduction",
                  href: "/docs",
                  description: "What prfct is and how it's organized.",
                },
                {
                  title: "Installation",
                  href: "/docs/installation",
                  description: "Add prfct to a Next.js project.",
                },
                {
                  title: "Foundations",
                  href: "/docs/foundations/color",
                  description: "Color, type, space, motion.",
                },
              ].map((item) => (
                <li key={item.title}>
                  <NavigationMenuLink
                    render={<Link href={item.href} />}
                    className="flex-col items-start gap-0.5 p-3"
                  >
                    <span className="font-medium text-foreground">
                      {item.title}
                    </span>
                    <span className="text-body-xs text-muted-foreground">
                      {item.description}
                    </span>
                  </NavigationMenuLink>
                </li>
              ))}
            </ul>
          </NavigationMenuContent>
        </NavigationMenuItem>
        <NavigationMenuItem>
          <NavigationMenuTrigger>Components</NavigationMenuTrigger>
          <NavigationMenuContent>
            <ul className="grid w-136 grid-cols-2 gap-1">
              {components.map((component) => (
                <li key={component.title}>
                  <NavigationMenuLink
                    render={<Link href={component.href} />}
                    className="flex-col items-start gap-0.5 p-3"
                  >
                    <span className="font-medium text-foreground">
                      {component.title}
                    </span>
                    <span className="line-clamp-2 text-body-xs text-muted-foreground">
                      {component.description}
                    </span>
                  </NavigationMenuLink>
                </li>
              ))}
            </ul>
          </NavigationMenuContent>
        </NavigationMenuItem>
        <NavigationMenuItem>
          <NavigationMenuLink
            render={<Link href="/themes" />}
            className={navigationMenuTriggerStyle()}
          >
            Themes
          </NavigationMenuLink>
        </NavigationMenuItem>
      </NavigationMenuList>
    </NavigationMenu>
  )
}

Installation

$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/navigation-menu.json

Usage

import Link from "next/link"

import {
  NavigationMenu,
  NavigationMenuContent,
  NavigationMenuItem,
  NavigationMenuLink,
  NavigationMenuList,
  NavigationMenuTrigger,
  navigationMenuTriggerStyle,
} from "@/components/ui/navigation-menu"
<NavigationMenu>
  <NavigationMenuList>
    <NavigationMenuItem>
      <NavigationMenuTrigger>Products</NavigationMenuTrigger>
      <NavigationMenuContent>
        <NavigationMenuLink render={<Link href="/analytics" />}>
          Analytics
        </NavigationMenuLink>
      </NavigationMenuContent>
    </NavigationMenuItem>
    <NavigationMenuItem>
      <NavigationMenuLink
        render={<Link href="/pricing" />}
        className={navigationMenuTriggerStyle()}
      >
        Pricing
      </NavigationMenuLink>
    </NavigationMenuItem>
  </NavigationMenuList>
</NavigationMenu>

All panels share a single popup. When people move from one trigger to the next, the popup morphs to the new panel's size and the content slides in from the direction of travel — a strong spatial cue that they're browsing siblings, not opening something new.

Examples

Mix triggers that open panels with plain links styled by navigationMenuTriggerStyle(), so every item in the bar looks and behaves consistently.

import Link from "next/link"

import {
  NavigationMenu,
  NavigationMenuContent,
  NavigationMenuItem,
  NavigationMenuLink,
  NavigationMenuList,
  NavigationMenuTrigger,
  navigationMenuTriggerStyle,
} from "@/components/ui/navigation-menu"

const components = [
  {
    title: "Button",
    href: "/docs/components/button",
    description: "Triggers an action. The most important interactive element.",
  },
  {
    title: "Dialog",
    href: "/docs/components/dialog",
    description: "A focused task on top of the page, with a clear way out.",
  },
  {
    title: "Tabs",
    href: "/docs/components/tabs",
    description: "Switch between related views without leaving the page.",
  },
  {
    title: "Command",
    href: "/docs/components/command",
    description: "Fast, keyboard-first search and actions.",
  },
  {
    title: "Select",
    href: "/docs/components/select",
    description: "Choose one option from a list of predefined values.",
  },
  {
    title: "Toast",
    href: "/docs/components/toast",
    description: "Brief, non-blocking feedback about an operation.",
  },
]

export default function NavigationMenuDemo() {
  return (
    <NavigationMenu>
      <NavigationMenuList>
        <NavigationMenuItem>
          <NavigationMenuTrigger>Getting started</NavigationMenuTrigger>
          <NavigationMenuContent>
            <ul className="grid w-120 grid-cols-[11rem_1fr] gap-1.5">
              <li className="row-span-3">
                <NavigationMenuLink
                  render={<Link href="/docs" />}
                  className="flex h-full flex-col items-start justify-end gap-1.5 rounded-lg bg-muted p-4 transition-colors duration-fast hover:bg-gray-4"
                >
                  <span className="text-heading-sm text-foreground">prfct</span>
                  <span className="text-body-xs text-muted-foreground">
                    Interfaces, perfected.
                  </span>
                </NavigationMenuLink>
              </li>
              {[
                {
                  title: "Introduction",
                  href: "/docs",
                  description: "What prfct is and how it's organized.",
                },
                {
                  title: "Installation",
                  href: "/docs/installation",
                  description: "Add prfct to a Next.js project.",
                },
                {
                  title: "Foundations",
                  href: "/docs/foundations/color",
                  description: "Color, type, space, motion.",
                },
              ].map((item) => (
                <li key={item.title}>
                  <NavigationMenuLink
                    render={<Link href={item.href} />}
                    className="flex-col items-start gap-0.5 p-3"
                  >
                    <span className="font-medium text-foreground">
                      {item.title}
                    </span>
                    <span className="text-body-xs text-muted-foreground">
                      {item.description}
                    </span>
                  </NavigationMenuLink>
                </li>
              ))}
            </ul>
          </NavigationMenuContent>
        </NavigationMenuItem>
        <NavigationMenuItem>
          <NavigationMenuTrigger>Components</NavigationMenuTrigger>
          <NavigationMenuContent>
            <ul className="grid w-136 grid-cols-2 gap-1">
              {components.map((component) => (
                <li key={component.title}>
                  <NavigationMenuLink
                    render={<Link href={component.href} />}
                    className="flex-col items-start gap-0.5 p-3"
                  >
                    <span className="font-medium text-foreground">
                      {component.title}
                    </span>
                    <span className="line-clamp-2 text-body-xs text-muted-foreground">
                      {component.description}
                    </span>
                  </NavigationMenuLink>
                </li>
              ))}
            </ul>
          </NavigationMenuContent>
        </NavigationMenuItem>
        <NavigationMenuItem>
          <NavigationMenuLink
            render={<Link href="/themes" />}
            className={navigationMenuTriggerStyle()}
          >
            Themes
          </NavigationMenuLink>
        </NavigationMenuItem>
      </NavigationMenuList>
    </NavigationMenu>
  )
}

A two-column list of destinations, each with an icon and a one-line description. Descriptions do the heavy lifting: they let people choose without clicking through.

import {
  ChartColumnIcon,
  DatabaseIcon,
  GlobeIcon,
  ShieldCheckIcon,
  WorkflowIcon,
  ZapIcon,
} from "lucide-react"
import Link from "next/link"

import {
  NavigationMenu,
  NavigationMenuContent,
  NavigationMenuItem,
  NavigationMenuLink,
  NavigationMenuList,
  NavigationMenuTrigger,
} from "@/components/ui/navigation-menu"

const products = [
  {
    title: "Analytics",
    description: "Understand usage in real time.",
    icon: ChartColumnIcon,
  },
  {
    title: "Automations",
    description: "Workflows that run themselves.",
    icon: WorkflowIcon,
  },
  {
    title: "Edge network",
    description: "Serve every region in under 50ms.",
    icon: GlobeIcon,
  },
  {
    title: "Storage",
    description: "Durable, versioned object storage.",
    icon: DatabaseIcon,
  },
  {
    title: "Security",
    description: "SSO, audit logs and SCIM.",
    icon: ShieldCheckIcon,
  },
  {
    title: "Functions",
    description: "Run code close to your users.",
    icon: ZapIcon,
  },
]

export default function NavigationMenuIconList() {
  return (
    <NavigationMenu>
      <NavigationMenuList>
        <NavigationMenuItem>
          <NavigationMenuTrigger>Products</NavigationMenuTrigger>
          <NavigationMenuContent>
            <ul className="grid w-144 grid-cols-2 gap-1">
              {products.map(({ title, description, icon: Icon }) => (
                <li key={title}>
                  <NavigationMenuLink
                    render={<Link href="/docs/components/navigation-menu" />}
                    className="items-start gap-3 p-3"
                  >
                    <span className="flex size-9 shrink-0 items-center justify-center rounded-lg border bg-card text-muted-foreground shadow-raised">
                      <Icon className="size-4" />
                    </span>
                    <span className="flex flex-col gap-0.5">
                      <span className="font-medium text-foreground">
                        {title}
                      </span>
                      <span className="text-body-xs text-muted-foreground">
                        {description}
                      </span>
                    </span>
                  </NavigationMenuLink>
                </li>
              ))}
            </ul>
          </NavigationMenuContent>
        </NavigationMenuItem>
      </NavigationMenuList>
    </NavigationMenu>
  )
}

Reserve a panel's side column for one timely item — a launch, a guide, an event. One feature per panel; two features compete.

import {
  ArrowRightIcon,
  BookOpenIcon,
  NewspaperIcon,
  PlayCircleIcon,
} from "lucide-react"
import Link from "next/link"

import { Badge } from "@/components/ui/badge"
import {
  NavigationMenu,
  NavigationMenuContent,
  NavigationMenuItem,
  NavigationMenuLink,
  NavigationMenuList,
  NavigationMenuTrigger,
} from "@/components/ui/navigation-menu"

const resources = [
  {
    title: "Documentation",
    description: "Guides and API reference.",
    icon: BookOpenIcon,
  },
  {
    title: "Changelog",
    description: "What shipped this week.",
    icon: NewspaperIcon,
  },
  {
    title: "Tutorials",
    description: "Step-by-step video lessons.",
    icon: PlayCircleIcon,
  },
]

export default function NavigationMenuFeatured() {
  return (
    <NavigationMenu>
      <NavigationMenuList>
        <NavigationMenuItem>
          <NavigationMenuTrigger>Resources</NavigationMenuTrigger>
          <NavigationMenuContent>
            <div className="grid w-128 grid-cols-[1fr_13rem] gap-1.5">
              <ul className="flex flex-col gap-1">
                {resources.map(({ title, description, icon: Icon }) => (
                  <li key={title}>
                    <NavigationMenuLink
                      render={<Link href="/docs" />}
                      className="items-start gap-3 p-3"
                    >
                      <Icon className="mt-0.5 text-muted-foreground" />
                      <span className="flex flex-col gap-0.5">
                        <span className="font-medium text-foreground">
                          {title}
                        </span>
                        <span className="text-body-xs text-muted-foreground">
                          {description}
                        </span>
                      </span>
                    </NavigationMenuLink>
                  </li>
                ))}
              </ul>
              <NavigationMenuLink
                render={<Link href="/docs/installation" />}
                className="flex-col items-start justify-between gap-6 rounded-lg border bg-surface p-4 hover:bg-accent"
              >
                <Badge variant="brand" size="sm">
                  New
                </Badge>
                <span className="flex flex-col gap-1">
                  <span className="font-medium text-foreground">
                    See what&apos;s new
                  </span>
                  <span className="text-body-xs text-muted-foreground">
                    Tokens, 50+ components and a theme builder.
                  </span>
                  <span className="mt-2 inline-flex items-center gap-1 text-label-sm text-link">
                    Read the announcement
                    <ArrowRightIcon className="size-3.5" />
                  </span>
                </span>
              </NavigationMenuLink>
            </div>
          </NavigationMenuContent>
        </NavigationMenuItem>
      </NavigationMenuList>
    </NavigationMenu>
  )
}

With no panels, the menu is a styled list of links. Every link stays a tab stop, and the arrow keys move between them too. Mark the current section with active — it sets aria-current="page" and the active style.

import Link from "next/link"

import {
  NavigationMenu,
  NavigationMenuItem,
  NavigationMenuLink,
  NavigationMenuList,
  navigationMenuTriggerStyle,
} from "@/components/ui/navigation-menu"

const links = [
  { title: "Overview", href: "/docs" },
  { title: "Components", href: "/docs/components" },
  { title: "Themes", href: "/themes" },
  { title: "Examples", href: "/examples" },
]

export default function NavigationMenuLinksOnly() {
  const current = "/docs/components"

  return (
    <NavigationMenu>
      <NavigationMenuList className="gap-1">
        {links.map((link) => (
          <NavigationMenuItem key={link.href}>
            <NavigationMenuLink
              active={link.href === current}
              render={<Link href={link.href} />}
              className={navigationMenuTriggerStyle({
                className: "data-active:bg-accent data-active:text-foreground",
              })}
            >
              {link.title}
            </NavigationMenuLink>
          </NavigationMenuItem>
        ))}
      </NavigationMenuList>
    </NavigationMenu>
  )
}

Guidelines

When to use

  • For the primary navigation of marketing sites and documentation with a handful of top-level sections.
  • When a section needs orientation — descriptions, grouping or a featured item — before people choose where to go.

When not to use

  • For app navigation with many destinations or deep hierarchies — use a Sidebar.
  • For commands and actions — use a Dropdown Menu or Menubar. Navigation menus contain links only.
  • On small screens — below md, collapse the navigation into a Sheet with a simple list.

Panel content

Keep five to seven top-level items. Every link in a panel gets a short title and, ideally, a one-line description in sentence case — punctuated the same way across the panel. Group long panels into columns of related links, and don't hide anything important only inside a panel — the destination should also be reachable from the page itself or the footer.

AnalyticsUnderstand usage in real time
AutomationsWorkflows that run themselves
DoTitles people recognize, with descriptions that help them choose.
Pulse
Flow Engine X
Nimbus
Don’tMarketing names with no context force people to click to find out.

Hover and click

Panels open on hover after a short delay (50ms) and on click. The popup and trigger are joined by an invisible bridge, so the pointer can travel diagonally into a panel without closing it. Never put a destination on the trigger itself — a trigger only opens its panel; the section's overview page belongs inside the panel as its first link.

Accessibility

Navigation Menu renders a <nav> with a list of items. Triggers are buttons with aria-expanded; panels are disclosed content, not menus — links inside are reached with Tab, just like on the page.

KeyBehavior
Tab
Moves through triggers and links, and into an open panel's links.
←→
Moves focus between top-level triggers and links.
EnterSpace
Opens or closes the focused trigger's panel. On a link, only Enter follows it, as on any link.
↓
On a trigger, opens its panel and moves focus into it.
Esc
Closes the panel and returns focus to its trigger.
  • Current page. Pass active to the link for the current page or section; it's exposed as aria-current="page".
  • Label multiple navs. If a page has more than one <nav>, give each an aria-label (Main, Footer) so screen reader users can tell them apart.

API reference

The root <nav>. Accepts every prop of the Base UI NavigationMenu.Root. After its children it renders NavigationMenuPositioner: the portal, positioner, popup and viewport that every panel shares. You don't place it yourself; it is exported for building a custom root, and takes side ("bottom"), sideOffset (8), align ("start") and alignOffset (0).

PropTypeDefault
value

Controlled value of the open item. Non-null means a panel is open.

anynull
defaultValue

Initially open item when uncontrolled.

anynull
onValueChange

Called when the open item changes.

(value: any, details) => voidNo default
delay

Hover delay before opening a panel, in ms.

number50
closeDelay

Delay before closing after the pointer leaves, in ms.

number50
orientation

Layout and arrow-key direction.

"horizontal" | "vertical""horizontal"
align

How the shared popup aligns against the active trigger.

"start" | "center" | "end""start"
PropTypeDefault
value

Identifies the item for controlled usage. Generated when omitted.

anyNo default

Opens its item's panel. Renders a chevron that turns while open. Accepts disabled.

The panel for an item. Size it with a width on its first child; the popup animates between panel sizes.

PropTypeDefault
keepMounted

Keep panel content in the DOM while closed, so crawlers can index its links.

booleanfalse
PropTypeDefault
active

Marks the current page: sets aria-current and the active style.

booleanfalse
closeOnClick

Close the open panel when the link is clicked.

booleanfalse
render

Render a framework link, e.g. <Link href="/pricing" />.

ReactElement | (props, state) => ReactElementNo default

A class generator that makes a plain NavigationMenuLink look exactly like a trigger. Accepts { className } to extend it.