ComponentsMenu
Navigation menu
A row of links to a site's or an app's sections, where some open a panel of further links.
Import
import { NavigationMenu, NavigationMenuList, NavigationMenuItem, NavigationMenuContent, NavigationMenuTrigger, NavigationMenuLink, NavigationMenuIndicator, NavigationMenuViewport, NavigationMenuSub } from "@booleanpress/ui/navigation-menu"Usage
A navigation menu is for moving between pages: its panels hold links, not commands. For commands, use a menubar or a dropdown menu.
import {
NavigationMenu,
NavigationMenuContent,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
NavigationMenuTrigger,
} from "@booleanpress/ui/navigation-menu"
export function SiteNav() {
return (
<NavigationMenu>
<NavigationMenuList>
<NavigationMenuItem>
<NavigationMenuTrigger>Logs</NavigationMenuTrigger>
<NavigationMenuContent>
<NavigationMenuLink href="/logs/delivery">Delivery log</NavigationMenuLink>
</NavigationMenuContent>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>
)
}Each NavigationMenuItem holds either a NavigationMenuTrigger with its NavigationMenuContent, or a plain NavigationMenuLink; give a top-level link className={navigationMenuTriggerStyle()} so it looks like the triggers. Panels open on hover and on click. By default every panel is shown in one shared viewport under the whole menu, which resizes and slides between panels; viewport={false} shows each panel under its own trigger instead. active on a link marks the current page (aria-current="page"). The panel's layout is yours: a list of links, a grid of groups for a mega menu, or a NavigationMenuSub for a second level that works like tabs. A link is a column (flex-col) for a title above a description; add flex-row items-center for an icon beside the text.
Examples
Basic
Three panels of links and a plain link styled as a trigger.
import {
NavigationMenu,
NavigationMenuContent,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
NavigationMenuTrigger,
navigationMenuTriggerStyle,
} from "@booleanpress/ui/navigation-menu"
const SECTIONS = [
{ title: "Sending", links: ["Connections", "Routing rules", "Suppression list"] },
{ title: "Logs", links: ["Delivery log", "Bounces", "Webhook events"] },
{ title: "Settings", links: ["General", "Notifications", "API keys"] },
]
export default function NavigationMenuBasic() {
return (
<div className="flex h-44 w-full items-start justify-center">
<NavigationMenu>
<NavigationMenuList>
{SECTIONS.map((section) => (
<NavigationMenuItem key={section.title}>
<NavigationMenuTrigger>{section.title}</NavigationMenuTrigger>
<NavigationMenuContent>
<ul className="flex w-48 flex-col gap-0.5">
{section.links.map((link) => (
<li key={link}>
<NavigationMenuLink href={`#${link.toLowerCase().replaceAll(" ", "-")}`}>{link}</NavigationMenuLink>
</li>
))}
</ul>
</NavigationMenuContent>
</NavigationMenuItem>
))}
<NavigationMenuItem>
<NavigationMenuLink href="#help" className={navigationMenuTriggerStyle()}>
Help
</NavigationMenuLink>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>
</div>
)
}Icons
Icons in the triggers and the links; viewport={false} puts each panel under its trigger.
import { ActivityIcon, BellIcon, KeyRoundIcon, LifeBuoyIcon, MailIcon, PlugIcon, ScrollTextIcon, SettingsIcon } from "lucide-react"
import {
NavigationMenu,
NavigationMenuContent,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
NavigationMenuTrigger,
navigationMenuTriggerStyle,
} from "@booleanpress/ui/navigation-menu"
const SECTIONS = [
{ title: "Sending", icon: MailIcon, links: [{ label: "Connections", icon: PlugIcon }, { label: "Delivery log", icon: ScrollTextIcon }] },
{ title: "Monitoring", icon: ActivityIcon, links: [{ label: "Alerts", icon: BellIcon }, { label: "Reports", icon: ActivityIcon }] },
{ title: "Settings", icon: SettingsIcon, links: [{ label: "API keys", icon: KeyRoundIcon }, { label: "General", icon: SettingsIcon }] },
]
export default function NavigationMenuIcons() {
return (
<div className="flex h-40 w-full items-start justify-center">
<NavigationMenu viewport={false}>
<NavigationMenuList>
{SECTIONS.map(({ title, icon: Icon, links }) => (
<NavigationMenuItem key={title}>
<NavigationMenuTrigger>
<Icon />
{title}
</NavigationMenuTrigger>
<NavigationMenuContent>
<ul className="flex w-44 flex-col gap-0.5">
{links.map(({ label, icon: LinkIcon }) => (
<li key={label}>
<NavigationMenuLink href={`#${label.toLowerCase().replaceAll(" ", "-")}`} className="flex-row items-center gap-2">
<LinkIcon />
{label}
</NavigationMenuLink>
</li>
))}
</ul>
</NavigationMenuContent>
</NavigationMenuItem>
))}
<NavigationMenuItem>
<NavigationMenuLink href="#support" className={navigationMenuTriggerStyle()}>
<LifeBuoyIcon />
Support
</NavigationMenuLink>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>
</div>
)
}Submenus
NavigationMenuSub with a vertical list: each area shows its own links beside it.
Mega menu
A wide panel of grouped links with a highlighted link across the bottom.
Accessibility
- Semantics
- The menu is a
navholding a list. Each trigger is abuttonwitharia-expandedandaria-controls; links areaelements, and the current page's link hasaria-current="page". It is a disclosure navigation, not an ARIA menu: there are no menu roles. - Labels
- Triggers and links are named by their text. Give the menu an
aria-labelwhen the page has more than onenav. - Focus
- Triggers and top-level links are tab stops; Tab from an open trigger moves into its panel. Keyboard focus is a 1px
--ringoutline 2px away on triggers and top-level links, and the--accentfill on links inside a panel. - Known limits
- The triggers carry no chevron, as the visual target; the state is in
aria-expanded. Add an icon in the trigger if your users need the cue. - Panels open on hover. A panel's links must not be the only way to reach those pages on touch devices: a tap opens the panel, a second tap on a link follows it.
- With the shared viewport, every panel opens under the menu's start edge (its right edge in right-to-left pages), not under its trigger;
viewport={false}puts each panel under its trigger.
- The triggers carry no chevron, as the visual target; the state is in
Keyboard
| Key | Behaviour |
|---|---|
| Tab | Moves to the next trigger or link; from an open trigger, into its panel. |
| EnterorSpace | On a trigger, opens or closes its panel. On a link, follows it. |
| โ | On an open trigger, moves into its panel. |
| โ | Moves to the next trigger or link in the row (โ in right-to-left pages). |
| โ | Moves to the previous trigger or link in the row (โ in right-to-left pages). |
| HomeorEnd | Moves to the first or last trigger or link in the row. |
| Escape | Closes the open panel and returns focus to its trigger. |
API
NavigationMenu
Renders Radix NavigationMenu.Root and passes it every other prop.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | ||
defaultValue | string | The panel open at the start, when it controls itself. | |
delayDuration | number | The duration from when the pointer enters the trigger until the tooltip gets opened. | |
dir | "ltr" | "rtl" | The reading direction. The provider's dir by default. | |
onValueChange | ((value: string) => void) | Called with the open item's value, or an empty string when it closes. | |
orientation | "horizontal" | "vertical" | horizontal (default) or vertical: which arrow keys move between items. | |
skipDelayDuration | number | How much time a user has to enter another trigger without incurring a delay again. | |
value | string | The open panel's value, when you control it. Pair it with onValueChange. | |
viewport | boolean | true | true (default) shows panels in one shared viewport under the menu; false under each trigger. |
NavigationMenuList
Renders Radix NavigationMenu.List and passes it every other prop.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean |
NavigationMenuItem
Renders Radix NavigationMenu.Item and passes it every other prop.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | ||
value | string | The value that names this item, for a controlled menu or a NavigationMenuSub. |
NavigationMenuContent
Renders Radix NavigationMenu.Content and passes it every other prop.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | ||
deferPointerDownOutside | boolean | When true, a 'pointerdown' event outside of the layered element will
wait for the interaction's click event before dispatching, allowing
third-party code to stop propagation of later events and cancel dismissal. | |
forceMount | true | Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries. | |
onEscapeKeyDown | ((event: KeyboardEvent) => void) | Event handler called when the escape key is down. Can be prevented. | |
onFocusOutside | ((event: FocusOutsideEvent) => void) | Event handler called when the focus moves outside of the DismissableLayer.
Can be prevented. | |
onInteractOutside | ((event: FocusOutsideEvent | PointerDownOutsideEvent) => void) | Event handler called when an interaction happens outside the DismissableLayer.
Specifically, when a pointerdown event happens outside or focus moves outside of it.
Can be prevented. | |
onPointerDownOutside | ((event: PointerDownOutsideEvent) => void) | Event handler called when the a pointerdown event happens outside of the DismissableLayer.
Can be prevented. |
NavigationMenuTrigger
Renders Radix NavigationMenu.Trigger and passes it every other prop.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | Render the child element instead, with this part's behaviour and classes merged onto it. |
NavigationMenuLink
Renders Radix NavigationMenu.Link and passes it every other prop.
| Prop | Type | Default | Description |
|---|---|---|---|
active | boolean | Marks the link to the current page, with aria-current="page". | |
asChild | boolean | Render the child element instead, with this part's behaviour and classes merged onto it. | |
onSelect | ((event: Event) => void) | Called when the link is followed. Call event.preventDefault() to keep the panel open. |
NavigationMenuIndicator
Renders Radix NavigationMenu.Indicator and passes it every other prop.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | ||
forceMount | true | Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries. |
NavigationMenuViewport
Renders Radix NavigationMenu.Viewport and passes it every other prop.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | ||
forceMount | true | Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries. |
NavigationMenuSub
Renders Radix NavigationMenu.Sub and passes it every other prop.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | ||
defaultValue | string | The item shown at the start. One item is always shown, as in tabs. | |
onValueChange | ((value: string) => void) | Called with the value of the item shown. | |
orientation | "horizontal" | "vertical" | horizontal (default) or vertical: which arrow keys move between its triggers. | |
value | string | The shown item's value, when you control it. Pair it with onValueChange. |
Also exported: navigationMenuTriggerStyle, a helper the parts use.
Every part takes className, merged with its defaults by cn(), and ref, which reaches the element it renders.
Data attributes: data-slot="navigation-menu" (NavigationMenu), data-slot="navigation-menu-list" (NavigationMenuList), data-slot="navigation-menu-item" (NavigationMenuItem), data-slot="navigation-menu-content" (NavigationMenuContent), data-slot="navigation-menu-trigger" (NavigationMenuTrigger), data-slot="navigation-menu-link" (NavigationMenuLink), data-slot="navigation-menu-indicator" (NavigationMenuIndicator), data-slot="navigation-menu-viewport-wrapper" (NavigationMenuViewport), data-slot="navigation-menu-sub" (NavigationMenuSub), and data-viewport.
Theming
Triggers have no fill at rest and --accent on hover, focus and while open. Panels are --popover with a --border edge; links use --accent when hovered or focused, and their icons --control-hover, then --muted-foreground.
The tokens its classes read. Change their values in your theme, and it follows.
| Token | Used for |
|---|---|
--accent | background |
--accent-foreground | text |
--border | background |
--control-hover | text |
--muted-foreground | text |
--popover | background |
--popover-foreground | text |
--ring | outline |