Nav Menu React component for Tailwind CSS v4
v1.0.1Updated in 1.6.0
A React navigation menu for websites: links and dropdown panels, lists or mega menus with a featured card, morphing through one panel.
Point at a menu and after a short delay its panel drops in; slide to the next one and the same panel glides under it, resizes to the new content and swaps it with a short slide in the direction you moved. A soft pill (or an underline) follows the pointer between triggers and rests on the section you are in. Click a link and the panel closes while the active item updates. With the keyboard, Tab reaches every trigger and link, ArrowLeft / ArrowRight move between triggers, Enter or Space opens a panel and Tab moves into it, ArrowDown opens it and lands on its first link, arrows move between its links and Escape closes it and returns focus. When the bar is narrower than the breakpoint (a phone) a hamburger takes the list's place: tap it and a full-screen menu slides in from the right with one accordion section per menu, 48px rows, the actions stacked full width at the bottom, and the section you are in already open. Choosing a link, tapping the close button, swiping the menu right or Escape closes it. Under reduced motion panels fade without sliding and resize at once.
Nav Menu is a free React component in the React navigation components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add nav-menu, together with Accordion, Badge, Button, Sheet, useControllableState and 3 more and the npm packages @radix-ui/react-navigation-menu, motion and lucide-react.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add nav-menuUsage
Import NavMenu from @/components/ui/nav-menu.
import { Calculator, FileText, Receipt } from "lucide-react"
import { Button } from "@/components/ui/button"
import { NavMenu, type NavMenuItem } from "@/components/ui/nav-menu"
import { NavLink } from "@/lib/navigation"
const items: NavMenuItem[] = [
{
label: "Product",
items: [
{ label: "Invoicing", href: "/billing", icon: <FileText />, description: "Invoices and estimates in 30 seconds" },
{ label: "E-invoicing", href: "/e-invoicing", icon: <Receipt />, description: "Sent and validated automatically" },
{ label: "Reports", href: "/reports", icon: <Calculator />, description: "Payments, sales tax and overdue" },
],
featured: {
title: "New: e-invoices sent automatically",
description: "Every invoice reaches your client with zero extra clicks.",
image: "/demo/laptop.jpg",
href: "/news/e-invoicing",
},
},
{ label: "Pricing", href: "/pricing" },
{ label: "Contact", href: "/contact" },
]
export function SiteHeader() {
return (
<header className="sticky top-0 z-sticky border-b border-foreground/10 bg-background px-4 md:px-6">
<NavMenu
items={items}
logo={<NavLink href="/" className="text-[15px] font-semibold">SimpleInvoice</NavLink>}
actions={
<>
<Button variant="ghost" size="sm">Log in</Button>
<Button size="sm">Try it free</Button>
</>
}
/>
</header>
)
}Props
items*NavMenuItem[]Top-level entries, the shared NavItem shape (id, label, href, icon, description, badge, disabled, external, match, onSelect, items) plus content (replaces the generated panel), featured ({ title, description?, image?, href, badge? }: a 16:10 card next to the links, a compact row in the phone menu) and columns (1 | 2 | 3; default 2 above 4 links, else 1). An item with items (or content) becomes a dropdown whose panel lists them; with an href as well, the panel starts with a "View all" row to it. Otherwise it is a link.
valuestringThe active item's key (navItemKey: id, else href, else the label), at any depth. Controls the active state.
defaultValuedefault""stringThe active item's key (navItemKey: id, else href, else the label) while uncontrolled and no current path is known; clicks then move it. Empty: the first enabled item. Ignored under a router: the playground drops its pretend router's path while this is set. Try /pricing or Solutions.
onValueChange(key: string, item: NavItem) => voidFires on every link or row activation, in every mode (controlled, router, uncontrolled).
currentPathdefault""stringThe current pathname. Empty: the LinkProvider's. With a path the router decides what is active (the longest matching href) and the top-level item above it gets the active look. Try /pricing or /product/reports.
logoReactNodeThe start slot, usually a link home. Repeated in the phone menu's header.
actionsReactNodeThe end slot ("Log in", "Try it free"). In the phone menu the same buttons stack full width at 48px, the last one on top.
mobileActionsReactNodeReplaces actions in the phone menu's footer.
viewportdefaulttruebooleanOne shared panel that follows the open trigger and morphs between menus (the Stripe header). Off: each panel opens as its own surface under its trigger.
indicatordefault"pill""pill" | "underline" | "none"The highlight that slides between hovered and open triggers on a smooth spring and rests on the active section: a soft pill, a 2px underline, or nothing (color only).
sizedefault"md""sm" | "md"Triggers 32 / 36px and the bar 56 / 64px. Triggers grow to 44px on touch screens.
aligndefault"center""start" | "center" | "end"Where the list sits between logo and actions. center keeps it in the middle of the bar.
mobileBreakpointdefault"md""md" | "lg"Below this width of the bar (768 / 1024px) the hamburger and the full-screen menu replace the list and actions, with CSS container queries, so the server HTML is already right. Use lg when the items do not fit a tablet.
delayDurationdefault150numberMilliseconds of hover before a panel opens, so a pointer passing over the bar does not flash menus.
skipDelayDurationdefault300numberMilliseconds after leaving a trigger during which the next one opens at once, without the delay.
defaultMobileOpendefaultfalsebooleanStarts with the phone menu open (uncontrolled).
mobileOpenbooleanThe phone menu's open state, controlled. Pair with onMobileOpenChange.
onMobileOpenChange(open: boolean) => voidFires when the phone menu opens or closes (hamburger, close button, a chosen link, swipe, Escape).
modaldefaulttruebooleanThe phone menu traps focus, locks the page scroll and dims the page. Turn it off for inline previews of the open menu.
containerHTMLElement | nullWhere the phone menu portals to, e.g. a phone frame. Default: document.body.
linkComponent(props: LinkComponentProps) => ReactNodeRenders every link through this component (a router Link) instead of the LinkProvider's.
labels{ navigation?: string; openMenu?: string; closeMenu?: string; menu?: string; viewAll?: string }Every string: the nav's aria-label ("Main"), the hamburger ("Open menu" / "Close menu"), the phone menu's hidden title ("Menu") and the first row of a panel whose item has its own page ("View all").
classNamesPartial<Record<"logo" | "list" | "item" | "trigger" | "link" | "chevron" | "indicator" | "viewport" | "content" | "panelLink" | "featured" | "actions" | "mobileTrigger" | "sheet" | "sheetLink" | "sheetActions", string>>Extra classes for the inner parts.
classNamestringExtra classes for the bar (the <nav>), merged last.
Parts
Examples
Panels under their triggers
viewport={false}: every menu opens its own panel under its trigger instead of one shared, morphing panel.
Underline indicator
indicator="underline" with the list at the start; the line follows the pointer and rests on Pricing.
Phone menu
Below the breakpoint the hamburger opens a full-screen menu: accordion sections, 48px rows, actions at the thumb.
App navigation
A compact two-level nav for an app (size="sm"); the router path /invoices marks Sales and its row active.
Over a dark hero photo
A transparent header on a photo: a dark scope turns the triggers white and the panel dark.
Dependencies
Files
- components/ui/nav-menu.tsx
FAQ
Is the React Nav Menu component free?
Yes. The source is free and needs no account: copy it from the Code tab or install it with the CLI.
How do I install Nav Menu?
Run npx wingo-ui@latest add nav-menu. It also adds the Wingo UI items it builds on (Accordion, Badge, Button, Sheet, useControllableState and 3 more) and the npm packages @radix-ui/react-navigation-menu, motion and lucide-react. With shadcn, run npx shadcn@latest add https://wingo-ui.com/r/nav-menu.json.
See also
Used in
More React navigation components
Articles using this component
- Shadcn Navbar: Mega Menu, Mobile Menu, Sticky Header
Build it: screens and apps9 min read