React Navigation Menu Component Examples
Drop-in React NavigationMenu examples: product header, dashboard nav, mega menu, icons with descriptions, and mobile drawer pairing. TypeScript.
A header dropdown does not need a state machine. Drivn's NavigationMenu renders a <nav> around a <ul> of <li> items, and each item carries Tailwind's group/item marker. The panel underneath starts at opacity-0 invisible and flips to visible through group-hover/item: and group-focus-within/item: variants, riding a transition-[opacity,visibility] duration-150 that eases in on the way out and out on the way in. No useState, no portal, no measured viewport — after hydration the browser drives the whole interaction.
The one piece of client code is an onPointerDown handler on NavigationMenu.Trigger. It checks whether the trigger is already document.activeElement and blurs it if so, which is what closes an open panel on a second tap where there is no hover state to lose. That handler is the only reason the file carries "use client".
Every example below imports from @/components/ui/navigation-menu, the path the Drivn CLI writes to, and composes through the dot-notation members List, Item, Trigger, Content, and Link attached with Object.assign. They cover a product header mixing a dropdown with a direct link, a dashboard menu with lucide icons, panels with two-line link descriptions, a two-column mega menu, and the responsive swap to a Drawer on phones. All TypeScript — Drivn ships TypeScript only. The prop tables live on the NavigationMenu docs page, and the head-to-head sits on Drivn vs shadcn NavigationMenu.
Product header with a dropdown and a direct link
The shape almost every marketing header lands on is a row where some items open a panel and some go straight to a page. Both live inside a NavigationMenu.Item: the dropdown item gets a NavigationMenu.Trigger followed by a NavigationMenu.Content, and the direct-link item gets a bare NavigationMenu.Link with neither. Because Item is the element carrying relative group/item, the two shapes sit on the same baseline and the flex items-center gap-1 on List spaces them identically — mixing them costs no layout work.
Trigger renders a <button> and appends a ChevronDown sized w-3.5 h-3.5 after whatever children you pass, so you write the label text and the caret arrives on its own. That caret rotates through group-hover/item:rotate-180 and group-focus-within/item:rotate-180, tracking the panel state without a prop being threaded down. The panel itself is absolute left-0 top-full min-w-[220px], anchored to its own item rather than to a shared viewport container, so two dropdowns never contend for one positioned surface. When a menu needs a single trigger and a flat list of actions, reach for Dropdown instead — NavigationMenu earns its markup once there are several panels in a row.
1 import { NavigationMenu } from "@/components/ui/navigation-menu" 2 import { LayoutGrid, MessageSquare } from "lucide-react" 3 4 export default function Header() { 5 return ( 6 <NavigationMenu> 7 <NavigationMenu.List> 8 <NavigationMenu.Item> 9 <NavigationMenu.Trigger> 10 Components 11 </NavigationMenu.Trigger> 12 <NavigationMenu.Content> 13 <NavigationMenu.Link href="/docs/components/card"> 14 <LayoutGrid /> 15 <span className="font-medium">Card</span> 16 </NavigationMenu.Link> 17 <NavigationMenu.Link href="/docs/components/dialog"> 18 <MessageSquare /> 19 <span className="font-medium">Dialog</span> 20 </NavigationMenu.Link> 21 </NavigationMenu.Content> 22 </NavigationMenu.Item> 23 <NavigationMenu.Item> 24 <NavigationMenu.Link href="/docs"> 25 Docs 26 </NavigationMenu.Link> 27 </NavigationMenu.Item> 28 </NavigationMenu.List> 29 </NavigationMenu> 30 ) 31 }
Links with descriptions
When the menu lists destinations that need a short description — "Installation: Get up and running in minutes", "Theming: Customize colors and design tokens" — stack the title and the description vertically inside the Link. Pass className="flex-col gap-0.5" to the NavigationMenu.Link so the default horizontal flex gap-3 flips to a column, then render a <span className="font-medium"> for the title and a <p className="text-xs text-muted-foreground"> for the description. The gap-0.5 keeps the title and the supporting line tight against each other while the py-2.5 from the base styles.link gives breathing room between rows.
This pattern reads like the Vercel and Linear marketing-site headers — wider panels with two-line links that orient the reader without forcing them to click through to find out what the destination is about. Combine it with the icon pattern from the previous section by adding the icon as the first child and wrapping the title plus description in a <div className="flex-1"> so the icon sits on the left and the text stack sits on the right. For tooltip-style descriptions that appear on hover instead of inside the panel, use the Tooltip component on a plain NavigationMenu.Link instead.
1 import { NavigationMenu } from "@/components/ui/navigation-menu" 2 3 export default function GettingStarted() { 4 return ( 5 <NavigationMenu> 6 <NavigationMenu.List> 7 <NavigationMenu.Item> 8 <NavigationMenu.Trigger>Getting Started</NavigationMenu.Trigger> 9 <NavigationMenu.Content> 10 <NavigationMenu.Link href="/docs/installation" className="flex-col gap-0.5"> 11 <span className="font-medium">Installation</span> 12 <p className="text-xs text-muted-foreground"> 13 Get up and running in minutes 14 </p> 15 </NavigationMenu.Link> 16 <NavigationMenu.Link href="/docs/theming" className="flex-col gap-0.5"> 17 <span className="font-medium">Theming</span> 18 <p className="text-xs text-muted-foreground"> 19 Customize colors and design tokens 20 </p> 21 </NavigationMenu.Link> 22 </NavigationMenu.Content> 23 </NavigationMenu.Item> 24 </NavigationMenu.List> 25 </NavigationMenu> 26 ) 27 }
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
The default open mechanism is group-hover/item plus group-focus-within/item on the parent Item, both pure CSS. To switch to click-only, lift the Content visibility into React state — wrap the Item in a small client component, track an open boolean in useState, and toggle it from a onClick handler on the Trigger. Replace the group-hover/item:opacity-100 rule in the local @/components/ui/navigation-menu.tsx source with a conditional className like open && "opacity-100 visible". The edit lives in your repo because Drivn copies the component source on install.
The directive exists because the Trigger runs an onPointerDown handler to dismiss focus when the user taps a trigger a second time on touch devices — without that handler, focus stays on the trigger and the group-focus-within/item rule keeps the panel open even after the tap. If you do not need the touch-tap-to-close behavior, delete the onPointerDown handler from the Trigger and remove the "use client" directive at the top of the file. The rest of the component is server-renderable.
Yes. The default NavigationMenu.Link renders a native <a> tag with the styled className applied. To swap to Next.js routing, edit the local @/components/ui/navigation-menu.tsx file and replace the <a> element in the Link function with import Link from "next/link" and <Link href={href} className={cn(styles.link, className)} {...props}>. The props spread keeps the href typing intact, and every existing call site continues to work because the dot-notation API does not change.
The default is min-w-[220px] from the styles.content class string, which fits a single column of links with reasonable padding. For mega-menu layouts override the className at the call site with min-w-[480px] or min-w-[640px] and pair it with a grid grid-cols-2 or grid-cols-3 layout. For panels with descriptive sub-text under each link, 320 to 400 pixels reads comfortably. The panel anchors to left-0 top-full of its item, so very wide panels under right-side triggers may need lg:left-auto lg:right-0 to keep the panel inside the viewport.
No. The keyboard model is native tab order — Tab moves to the next focusable element on the page, which is the next Trigger button. To match the arrow-key menu pattern from the WAI-ARIA "menubar" role, add an onKeyDown handler to the NavigationMenu.List that intercepts ArrowLeft and ArrowRight and shifts focus between sibling triggers. The local source file is forty lines, so the edit is scoped to one component file rather than a runtime override. For application command menus where arrow navigation is the expected affordance, the shadcn/ui NavigationMenu wraps Radix and ships the pattern out of the box.