Skip to content

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.

Navigation10 propsAccessibility notes

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.tsx
<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

items

NavItemData[]

default: —

{ key, label, href?, icon?, badge?, children?, isDisabled?, isSection? }.

aria-label

string

default: —

Required.

Behavior & states

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

isCollapsed

boolean

default: false

Icon rail + hover/focus flyouts for groups.

Styling

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.