Navigation menu
FreeA site header whose items open frosted panels of links. Hover waits for intent, the panel resizes from one item to the next, and every link is reachable by keyboard.
pnpm dlx shadcn@latest add https://caliperui.site/r/navigation-menu.jsonWhen to use #
Use it for
- Marketing and docs sites with product and resource sections.
- Headers with more destinations than fit as plain links.
Skip it when
- Phones; fold the links into a drawer as the demo does.
- App navigation; use the sidebar.
Installation #
pnpm dlx shadcn@latest add https://caliperui.site/r/navigation-menu.json
Adds its 3 files to components/ui/, each with its own stylesheet, and installs their packages. No account needed. The first part also adds Caliper UI’s tokens; import them once as in Installation.
Usage #
This is the code behind the preview above. It is built from NavigationMenu, NavigationMenuList, NavigationMenuItem, NavigationMenuTrigger, NavigationMenuContent, NavigationMenuLink, NavigationMenuCard and NavigationMenuPanel.
import { Boxes, ChartLine, GitBranch, ShieldCheck, Workflow } from "lucide-react" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Drawer, DrawerContent, DrawerDescription, DrawerTitle, DrawerTrigger } from "@/components/ui/drawer" import { NavigationMenu, NavigationMenuContent, NavigationMenuItem, NavigationMenuLink, NavigationMenuList, NavigationMenuPanel, NavigationMenuTrigger } from "@/components/ui/navigation-menu" const product = [ { title: "Platform", links: [ { href: "#ledger", title: "Ledger", description: "Every entry, reconciled as it lands.", icon: <Boxes /> }, { href: "#close", title: "Close", description: "A month-end checklist that runs itself.", icon: <Workflow /> }, { href: "#reports", title: "Reports", description: "Board-ready numbers on the first day.", icon: <ChartLine /> }, ], }, { title: "Trust", links: [ { href: "#security", title: "Security", description: "SOC 2 Type II, SSO and audit trails.", icon: <ShieldCheck /> }, { href: "#changelog", title: "Changelog", description: "What shipped this week.", icon: <GitBranch /> }, ], }, ]; const resources = [ { links: [ { href: "#guides", title: "Guides", description: "Set up your first close in an afternoon." }, { href: "#api", title: "API reference", description: "REST and webhooks with examples." }, { href: "https://status.example.com", title: "System status", description: "Uptime for the last 90 days.", external: true }, ], }, ]; export function NavigationMenuDemo() { return ( <div className="w-full"> <header className="flex h-14 items-center justify-between gap-4 rounded-[14px] border border-border px-4"> <span className="flex items-center gap-2 text-sm font-semibold"> <span aria-hidden="true" className="grid size-7 place-items-center rounded-[8px] bg-foreground text-xs text-background">H</span> Halden </span> <NavigationMenu aria-label="Main" className="max-md:hidden"> <NavigationMenuList> <NavigationMenuItem> <NavigationMenuTrigger>Product</NavigationMenuTrigger> <NavigationMenuContent> <NavigationMenuPanel columns={product} featured={ <> <Badge variant="info">New</Badge> <p className="text-sm font-medium">Two-way NetSuite sync</p> <p className="text-xs leading-[18px] text-muted-foreground">Entries posted in either place appear in both within a minute.</p> <a href="#netsuite" className="text-xs font-medium text-foreground underline underline-offset-2">Read the launch note</a> </> } /> </NavigationMenuContent> </NavigationMenuItem> <NavigationMenuItem> <NavigationMenuTrigger>Resources</NavigationMenuTrigger> <NavigationMenuContent> <NavigationMenuPanel columns={resources} /> </NavigationMenuContent> </NavigationMenuItem> <NavigationMenuItem> <NavigationMenuLink href="#customers">Customers</NavigationMenuLink> </NavigationMenuItem> <NavigationMenuItem> <NavigationMenuLink href="#pricing" active>Pricing</NavigationMenuLink> </NavigationMenuItem> </NavigationMenuList> </NavigationMenu> <div className="flex items-center gap-2"> <Button size="sm" className="max-md:hidden">Start free</Button> <Drawer> <DrawerTrigger asChild> <Button size="sm" variant="secondary" className="md:hidden">Menu</Button> </DrawerTrigger> <DrawerContent> <DrawerTitle>Menu</DrawerTitle> <DrawerDescription className="sr-only">Site navigation</DrawerDescription> <nav aria-label="Main" className="grid gap-5 overflow-y-auto"> {[...product, { title: "Resources", links: resources[0]!.links }].map((group) => ( <div key={group.title} className="grid gap-1"> <p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">{group.title}</p> {group.links.map((link) => ( <a key={link.href} href={link.href} className="rounded-[10px] px-2 py-2.5 text-sm hover:bg-muted">{link.title}</a> ))} </div> ))} <Button>Start free</Button> </nav> </DrawerContent> </Drawer> </div> </header> <div className="h-72" aria-hidden="true" /> </div> ); }
API reference #
Accessibility #
It is built on native elements and keeps aria-hidden and aria-current in step with state.
Keyboard and focus handling comes from Radix navigation-menu: arrow keys move through items, Enter and Space select, and Escape closes and returns focus.
Motion #
Transitions use the shared duration and easing tokens. Under prefers-reduced-motion those tokens collapse to instant and travel distances go to zero, so state changes still happen without movement.