Nav component
Nav is a multi-level navigation tree with collapsible groups, active-route styling, an optional accordion mode, and a collapsed icon-rail mode. It is polymorphic over your router's link component.
Playground
Change a prop on the right and the preview updates. Switch to Code to copy what you see.
Recipes
Copy-paste patterns for the ways Nav actually gets used — each one a real, self-contained example.
Basic navigation
Pass items and a renderItem that returns your router's link. isItemActive marks the current route — the active item gets aria-current="page".
<Nav
aria-label="Main"
items={[
{ key: "home", label: "Home", href: "/" },
{ key: "projects", label: "Projects", href: "/projects" },
{ key: "team", label: "Team", href: "/team" },
]}
isItemActive={(item) => item.href === pathname}
renderItem={({ item, content, props }) => (
<Link
href={item.href}
className={props.className}
aria-current={props["aria-current"]}
aria-disabled={props["aria-disabled"]}
>
{content}
</Link>
)}
/>
// The other recipes reuse this as `renderLink` — the same function pulled out.Icons and badges
Each item can carry an icon and a badge — a count of unread items, a “New” tag — rendered inline with the label.
<Nav
aria-label="Main"
items={[
{ key: "home", label: "Home", href: "/", icon: <HomeIcon /> },
{ key: "inbox", label: "Inbox", href: "/inbox", icon: <InboxIcon />, badge: 12 },
{ key: "team", label: "Team", href: "/team", icon: <UsersIcon /> },
]}
renderItem={renderLink}
/>Multi-level menu
children nests to any depth -- a group item (no href, just children) renders a toggle with a caret; clicking it expands/collapses that branch, same as its children can. defaultExpandedKeys opens a branch initially, and the active leaf's ancestors auto-expand.
<Nav
aria-label="Main"
defaultExpandedKeys={["reports"]}
items={[
{ key: "dashboard", label: "Dashboard", href: "/dashboard" },
{
key: "reports",
label: "Reports",
children: [
{ key: "reports-weekly", label: "Weekly", href: "/reports/weekly" },
{ key: "reports-monthly", label: "Monthly", href: "/reports/monthly" },
{
key: "reports-custom",
label: "Custom",
children: [
{ key: "reports-custom-saved", label: "Saved views", href: "/reports/custom/saved" },
{ key: "reports-custom-new", label: "New report", href: "/reports/custom/new" },
],
},
],
},
{ key: "settings", label: "Settings", href: "/settings" },
]}
renderItem={renderLink}
/>Grouped sections
isSection marks a group header; its children nest beneath it. expansionMode="single" makes the groups behave like an accordion.
<Nav
aria-label="Workspace"
expansionMode="single"
items={[
{
key: "workspace",
label: "Workspace",
isSection: true,
children: [
{ key: "dashboard", label: "Dashboard", href: "/dashboard" },
{ key: "reports", label: "Reports", href: "/reports" },
],
},
{
key: "settings",
label: "Settings",
isSection: true,
children: [
{ key: "profile", label: "Profile", href: "/settings/profile" },
{ key: "billing", label: "Billing", href: "/settings/billing" },
],
},
]}
renderItem={renderLink}
/>Collapsed icon rail
isCollapsed shrinks the nav to an icon rail with hover/focus flyouts — each item keeps an accessible name. Give items icons for this mode.
<Nav
aria-label="Main"
isCollapsed
items={[
{ key: "home", label: "Home", href: "/", icon: <HomeIcon /> },
{ key: "inbox", label: "Inbox", href: "/inbox", icon: <InboxIcon /> },
{ key: "team", label: "Team", href: "/team", icon: <UsersIcon /> },
{ key: "settings", label: "Settings", href: "/settings", icon: <SettingsIcon /> },
]}
renderItem={renderLink}
/>Disabled item
isDisabled on an item removes it from keyboard navigation and marks it unavailable — a feature gated behind a plan, say.
<Nav
aria-label="Main"
items={[
{ key: "home", label: "Home", href: "/" },
{ key: "reports", label: "Reports", href: "/reports" },
{ key: "api", label: "API access (Pro)", href: "/api", isDisabled: true },
]}
renderItem={renderLink}
/>Example
<Nav
items={items}
isItemActive={(item) => item.href === pathname}
renderItem={({ item, content, props }) =>
item.href ? <Link href={item.href} {...props}>{content}</Link>
: <span {...props}>{content}</span>
}
/>Props
The full public API for Nav. Every prop is stable and intentionally typed.
Data
| Prop | Type | Default | Description |
|---|---|---|---|
| items | NavItemData[] | — | { key, label, href?, icon?, badge?, children?, isDisabled?, isSection? }. |
| aria-label | string | — | Required. |
items
NavItemData[]
default: —
{ key, label, href?, icon?, badge?, children?, isDisabled?, isSection? }.
aria-label
string
default: —
Required.
Behavior & states
| Prop | Type | Default | Description |
|---|---|---|---|
| selectedKey | string | — | Which leaf is the current page. |
| isItemActive | (item) => boolean | — | Custom active check (wins over selectedKey). |
| onItemSelect | (item) => void | — | Fired on leaf / link activation. |
| expandedKeys / defaultExpandedKeys / onExpandedChange | Iterable<string> / … | — | Controlled / uncontrolled expansion (active-leaf ancestors auto-expand). |
| expansionMode | "single" | "multiple" | multiple | "single" = accordion per section. |
| renderItem | (args) => ReactNode | — | Full control over each row — for router NavLink, etc. |
selectedKey
string
default: —
Which leaf is the current page.
isItemActive
(item) => boolean
default: —
Custom active check (wins over selectedKey).
onItemSelect
(item) => void
default: —
Fired on leaf / link activation.
expandedKeys / defaultExpandedKeys / onExpandedChange
Iterable<string> / …
default: —
Controlled / uncontrolled expansion (active-leaf ancestors auto-expand).
expansionMode
"single" | "multiple"
default: multiple
"single" = accordion per section.
renderItem
(args) => ReactNode
default: —
Full control over each row — for router NavLink, etc.
Appearance
| Prop | Type | Default | Description |
|---|---|---|---|
| isCollapsed | boolean | false | Icon rail + hover/focus flyouts for groups. |
isCollapsed
boolean
default: false
Icon rail + hover/focus flyouts for groups.
Styling
| Prop | Type | Default | Description |
|---|---|---|---|
| classNames | { base?, list?, item?, link?, sectionTitle? } | — | Slot class overrides. |
classNames
{ base?, list?, item?, link?, sectionTitle? }
default: —
Slot class overrides.
Accessibility
- Renders as `<nav>` containing a list; the active item carries `aria-current="page"`.
- Group toggles are buttons with `aria-expanded`; keyboard users can open and close branches.
- In collapsed icon-rail mode each item keeps an accessible name via `aria-label` or a tooltip.