Navigation Menu
Open site sections from a row of links with dropdown panels.
Installation
pnpm dlx shadcn@4.18.0 add @siteplane/navigation-menuComplete the one-time setup first. Registry JSON lists source files and dependencies.
Usage
import { useRef } from "react";
import {
NavigationMenu,
NavigationMenuContent,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
NavigationMenuPopup,
NavigationMenuTrigger,
} from "@/siteplane/components/navigation-menu";
function SiteNavigation() {
const anchor = useRef<HTMLUListElement>(null);
return (
<NavigationMenu aria-label="Main navigation">
<NavigationMenuList ref={anchor}>
<NavigationMenuItem value="product">
<NavigationMenuTrigger>Product</NavigationMenuTrigger>
<NavigationMenuContent>
<div className="w-80 p-4">
<NavigationMenuLink
description="See your workspace at a glance."
href="#overview"
>
Overview
</NavigationMenuLink>
</div>
</NavigationMenuContent>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuLink href="#pricing">Pricing</NavigationMenuLink>
</NavigationMenuItem>
</NavigationMenuList>
<NavigationMenuPopup anchor={anchor} />
</NavigationMenu>
);
}Composition
NavigationMenu
├── NavigationMenuList
│ ├── NavigationMenuItem
│ │ ├── NavigationMenuTrigger
│ │ └── NavigationMenuContent
│ │ └── NavigationMenuLink (panel row)
│ └── NavigationMenuItem
│ └── NavigationMenuLink
└── NavigationMenuPopup (shared positioner, popup and viewport)A link inside NavigationMenuContent takes the full-width panel row style on its own, so it needs no popup flag, and a link beside the triggers keeps the style of the bar. Links rest in the secondary text color, and hover draws a light fill. The link of the current page, marked with active, keeps a persistent fill and the full foreground, the same as the current row of a Sidebar.
Header Navigation builds its desktop links from these parts.
Multi-column panels
Content is plain markup, so a panel can hold several labeled groups. Give the content element a width, and keep it max-w-full so it never outgrows the screen. Moving between triggers keeps one popup open: it resizes to the new content while the old and the new panel cross-fade.
Controlled value
Pass value and onValueChange to open and close panels from your own state. Match value to the value of a NavigationMenuItem; null closes the menu.
Open item: none
API reference
| Prop | Type | Default | Description |
|---|---|---|---|
Navigation | Base UI NavigationMenu.Root props | nav | Navigation landmark. Give it an aria-label; the other props follow Base UI. |
Navigation | string | null | null | Controlled or initial open item, matched to NavigationMenuItem value. null means closed. |
Navigation | (value, eventDetails) => void | unset | Called when an item opens or closes. |
Navigation | number | 50 / 50 | Milliseconds before hover opens or closes a panel. |
Navigation | ul props | flex row, 4 px gap | Row of items. Pass a ref to anchor the popup to the whole row. |
Navigation | string | generated | Identifies the item. Set it when you control the menu. |
Navigation | Base UI Trigger props | ghost Button, navigation-link profile | Opens the item's panel on hover or press and adds a chevron that turns while it is open. |
Navigation | string | unset | Destination of the anchor. |
Navigation | boolean | false | Marks the current page with aria-current page and shows the persistent fill with the full foreground. |
Navigation | boolean | true inside NavigationMenuContent, false elsewhere | Panel row style: full width and aligned to the start. Set false for a bar link inside a panel. |
Navigation | ReactNode | unset | Supporting text under the label. Beside a leading icon it starts under the label text. |
Navigation | boolean | true | Closes the open panel when the link is pressed. |
Navigation | boolean | false | Keeps the panel in the DOM while closed, so it exists in server-rendered markup. |
Navigation | Base UI Positioner anchor | trigger | Element the shared popup aligns to. Pass the list ref to align it with the row. |
Parts accept the props of their Base UI counterparts and string className.
Trigger and link render Buttons, so they share the Button's focus, hover and
pressed states. A render prop on either replaces the element inside that
Button, such as the link's anchor with your router's link, and the Button keeps
its styling; a render function receives the part's state. The popup renders in a
portal 8 px below the row, aligned to its start edge, with 16 px of collision
padding. Render exactly one NavigationMenuPopup inside NavigationMenu.
Usage notes
Use a Navigation Menu for site-level destinations with panels of links. Use a Menu for actions, and Tabs to switch content within a page. Triggers only open panels; put every destination inside a panel or beside the triggers as a link.
The Left and Right arrow keys move between the triggers and links of the row. Enter or Space on a trigger opens its panel and moves focus to the first link, the Up and Down arrow keys move through the links, and Escape closes the panel and returns focus to the trigger. Set active on the link of the current page so it is announced with aria-current="page" and keeps its persistent fill.
Use real destination URLs in your application. These examples use fragment destinations to keep the preview local. Opening and switching panels fades and moves the content a few pixels; reduced motion removes the movement.