Next.js Navigation Menu with Dropdowns
Add a Next.js navigation menu with dropdown panels that open on hover and keyboard focus. Drivn ships one client component, CSS-only disclosure, zero state.
A top navigation menu is the spine of a marketing site or a docs app — the horizontal bar of triggers that drop panels of links to your product, pricing, and documentation. In the Next.js App Router the interesting question is where the client boundary falls: a menu that opens on hover and keyboard focus needs to run in the browser, yet the routes it points at are App Router pages you want to reach without a full reload. Drivn's Navigation Menu draws that line cleanly.
After install it lives in src/components/ui/navigation-menu.tsx as a small family of functions wired together with Object.assign — the root plus .List, .Item, .Trigger, .Content, and .Link. It carries a 'use client' directive because the trigger runs an onPointerDown handler that toggles focus, but it holds no useState: the panels open and close entirely through CSS, using group-hover/item and group-focus-within/item selectors on the styles.content class. The disclosure logic ships as Tailwind classes, not JavaScript state, and it keeps working for keyboard and pointer users alike.
This guide installs Drivn in a Next.js 16 project, renders the menu bar, fills a dropdown panel with links to App Router routes, explains the hover-and-focus disclosure, and restyles the styles object you own. Every snippet comes from the component's real API. For the full reference see the Navigation Menu docs; for the shadcn/ui comparison see Drivn vs shadcn/ui Navigation Menu.
Install in a Next.js 16 project
Drivn installs through a small CLI that writes component source directly into your repository — there is no runtime package to version-lock. From the root of your Next.js 16 project run npx drivn add navigation-menu. The component imports the ChevronDown glyph from lucide-react for the trigger caret, so keep that package installed; the CLI writes a single navigation-menu.tsx file and leaves the rest of your tree untouched. The CLI reference documents every flag, including targeting a custom directory or installing several components at once. After install you own the file, and future Drivn releases will not overwrite it. Commit it for a clean baseline before wiring the menu into your layout.
1 # from the root of your Next.js 16 project 2 npx drivn add navigation-menu
Link panels to App Router routes
Each NavigationMenu.Link renders a native <a href> styled with the styles.link class — a flex w-full gap-3 row with hover and focus-visible rings. Point href at any App Router route and the link works immediately. One Next.js nuance matters: a native anchor performs a full-document navigation, so for the soft, client-side transitions the App Router is known for, render Next's <Link> inside the NavigationMenu.Item and hand it the same styles.link classes. For external URLs — a status page, a changelog on another domain — the native NavigationMenu.Link is exactly right. You can mix both inside one NavigationMenu.Content panel: Next <Link> for in-app routes, NavigationMenu.Link for anything off-site. The panel itself is a min-w-[220px] card with bg-card border border-border rounded-xl p-2, so fill it with icons, stacked descriptions, or a custom grid — every NavigationMenu.Link accepts children and merges an extra className for per-link layout tweaks.
1 import Link from 'next/link' 2 import { NavigationMenu } from '@/components/ui/navigation-menu' 3 4 export function ResourcesMenu() { 5 return ( 6 <NavigationMenu> 7 <NavigationMenu.List> 8 <NavigationMenu.Item> 9 <NavigationMenu.Trigger>Resources</NavigationMenu.Trigger> 10 <NavigationMenu.Content> 11 {/* in-app route: soft client-side navigation */} 12 <Link 13 href="/docs/installation" 14 className="flex w-full gap-3 px-3 py-2.5 text-sm font-medium text-foreground rounded-lg hover:bg-accent transition-colors" 15 > 16 Installation 17 </Link> 18 {/* off-site link: native anchor is correct */} 19 <NavigationMenu.Link href="https://github.com/drivn"> 20 GitHub 21 </NavigationMenu.Link> 22 </NavigationMenu.Content> 23 </NavigationMenu.Item> 24 </NavigationMenu.List> 25 </NavigationMenu> 26 ) 27 }
Hover-and-focus disclosure, no state
The panels open with no JavaScript state. Each NavigationMenu.Item carries group/item, and the styles.content class starts at opacity-0 invisible and flips to opacity-100 visible under both group-hover/item: and group-focus-within/item: — so the same panel appears whether the user hovers the trigger with a pointer or tabs into it with the keyboard. The focus-within half is what keeps the menu accessible: a keyboard user tabs to the trigger, the panel becomes visible because focus is inside the item, and the links inside become reachable in order. The transition runs on opacity and visibility with ease-in on close and ease-out on open, so the panel fades rather than snapping. Because the logic is Tailwind classes rather than a useState boolean, there is no hydration cost for the open and close behavior and nothing to fall out of sync between the server and client render.
1 // styles.content — CSS-only disclosure (verbatim from navigation-menu.tsx) 2 content: cn( 3 'absolute left-0 top-full min-w-[220px] z-50', 4 'bg-card border border-border rounded-xl p-2', 5 'shadow-lg shadow-black/8', 6 'origin-[var(--origin)]', 7 'transition-[opacity,visibility]', 8 'duration-150 ease-in', 9 'opacity-0 invisible', 10 'group-hover/item:opacity-100', 11 'group-hover/item:visible', 12 'group-hover/item:ease-out', 13 'group-focus-within/item:opacity-100', 14 'group-focus-within/item:visible', 15 'group-focus-within/item:ease-out' 16 ),
Customize the styles object
Every class the menu renders lives in one styles object at the top of the file you own after install — root, list, item, trigger, triggerIcon, content, and link. Restyle the trigger's hover state by editing trigger, widen the panel by bumping min-w-[220px] in content, or change the caret spin through triggerIcon, whose group-hover/item:rotate-180 and group-focus-within/item:rotate-180 rules turn the ChevronDown when the panel opens. Because the colors read from Tailwind tokens — bg-card, border-border, bg-accent — changing them in theming re-themes the whole menu for dark and light mode at once. Edit the object in place and every menu in your app updates together. For copy-paste panel layouts see the Navigation Menu examples.
1 // styles.triggerIcon — caret rotation (verbatim) 2 triggerIcon: cn( 3 'w-3.5 h-3.5 transition-transform duration-200', 4 'group-hover/item:rotate-180', 5 'group-focus-within/item:rotate-180' 6 ),
Install Drivn in one command
Copy the source into your project and own every line. Zero runtime dependencies, pure React + Tailwind.
npx drivn@latest createRequires Node 18+. Works with npm, pnpm, and yarn.
Frequently asked questions
No — the Navigation Menu source carries a 'use client' directive because the trigger runs an onPointerDown handler that manually toggles focus so a tap opens and closes the panel on touch devices. Render it from a client component or place it in a layout that Next.js can treat as a client boundary. The panels themselves open through CSS, but the pointer handler needs the browser, so the component as a whole is a client component. The App Router routes it links to stay server-rendered as normal.
The built-in NavigationMenu.Link renders a native <a href>, which performs a full-document navigation. To get the App Router's soft, client-side transitions, render Next's <Link> inside the NavigationMenu.Item or NavigationMenu.Content and give it the same styles.link utility classes so it matches visually. Reserve the native NavigationMenu.Link for external URLs like a status page or a docs site on another domain, where a hard navigation is what you actually want. You can mix both kinds of link inside a single panel.
Yes. The styles.content panel is hidden with opacity-0 invisible and revealed under two selectors — group-hover/item: for pointer users and group-focus-within/item: for keyboard users. When someone tabs to the trigger, focus lands inside the item, focus-within matches, and the panel becomes visible so its links enter the tab order. That means the same markup serves mouse and keyboard navigation without a separate handler, and there is no useState toggle to keep in sync between the two input modes.
Open navigation-menu.tsx after install and edit the styles.content string — bump min-w-[220px] for a wider panel, swap bg-card or border-border for different surface tokens, or adjust rounded-xl and p-2 for a different shape. Because the panel reads its colors from Tailwind tokens, changing those tokens in theming re-themes every menu at once for dark and light mode. The whole visual surface lives in one styles object, so a width or color change is a single edit in the file you own.
Yes. Every NavigationMenu.Link accepts children, so drop a Lucide icon and a <span> label inside it, or stack a bold title over a muted <p> description for a richer panel. The link's base class is flex w-full gap-3, which lays icon and text out in a row; add a className like flex-col gap-0.5 on an individual link for a stacked title-and-description layout. The panel is a plain card, so any custom grid or multi-column layout you build inside NavigationMenu.Content works.

