Skip to content
Nizivo UI

NavItem

One sidebar destination, as a native link with an icon, a label and an optional count. The current one is aria-current="page"; a router's link goes in through render. In a Sidebar's icon rail it folds to its icon, with no prop; SidebarItem adds the rail's Tooltip.

Server component, no client JS

Import

import { NavItem } from '@nizivo/react/nav-item';

// Once per app (Getting started), after
// @nizivo/tokens/tokens.css and @nizivo/react/css/base.css
import '@nizivo/react/css/nav-item.css';

Static or interactive

Server. It ships no client JavaScript when a Server Component renders it, in every Next.js build mode; its code is bundled only when a client component (a 'use client' file) imports it. It is a plain <a>; a router link passed through render is the router's own code. The page says which item is current, because it knows its own route (a router's usePathname needs a client component). How to choose.

In a client app (a Vite or React Router single-page app, or a 'use client' file) it is an ordinary React component: event handlers, refs and state work as usual. In a client app.

A list of items

NavItem is one link. Put the items in a <nav> with an aria-label, around a <ul> with one <li> per item, so assistive technology announces a navigation landmark and the number of destinations. A list of NavItems needs no component of its own.

The collapsed rail

Collapsed is not a prop. In a Sidebar folded to the icon rail (from 640 px to 1280 px, or with collapsed), each item becomes a 40 px square around its icon. The label and the count leave the layout but stay in the DOM, so the link keeps its accessible name ("Orders 12"), and a count shows as a dot on the icon. In a Sidebar, use SidebarItem from @nizivo/react/sidebar: the same link, with a Tooltip that shows its label in the rail (the example below). Give every item an icon in a sidebar that collapses.

Height and hover

The row is a fixed 40 px with or without a count (Figma's height followed its content: 40 with the count pill, 38 without). A long label ends in an ellipsis. The hover fill is bg/muted: Figma's bg/hover equals the sidebar's bg/canvas in light mode, so its hover could not be seen.

Examples

Basic

Sidebar destinations: a <nav> around a list of links. The current one is aria-current="page"; a count follows the label and is part of the link's name.

import type { CSSProperties, JSX, ReactNode } from 'react';
import { NavItem } from '@nizivo/react/nav-item';

/** Stands in for an `@nizivo/icons` glyph (20 px Lucide strokes, sized by the slot). */
function Glyph({ children }: { children: ReactNode }): JSX.Element {
  return (
    <svg
      viewBox="0 0 24 24"
      width="1em"
      height="1em"
      fill="none"
      stroke="currentColor"
      strokeWidth={1.92}
      strokeLinecap="round"
      strokeLinejoin="round"
    >
      {children}
    </svg>
  );
}

const ICONS = {
  home: (
    <Glyph>
      <path d="M3 10.5 12 3l9 7.5V21H3z" />
      <path d="M9 21v-7h6v7" />
    </Glyph>
  ),
  orders: (
    <Glyph>
      <path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z" />
      <path d="M3 6h18M16 10a4 4 0 0 1-8 0" />
    </Glyph>
  ),
  products: (
    <Glyph>
      <path d="m21 8-9-5-9 5v8l9 5 9-5z" />
      <path d="m3 8 9 5 9-5M12 13v8" />
    </Glyph>
  ),
  customers: (
    <Glyph>
      <circle cx="9" cy="8" r="4" />
      <path d="M2 21a7 7 0 0 1 14 0M16 4a4 4 0 0 1 0 8M22 21a7 7 0 0 0-4-6.3" />
    </Glyph>
  ),
  settings: (
    <Glyph>
      <circle cx="12" cy="12" r="3" />
      <path d="M12 2v3M12 19v3M2 12h3M19 12h3M4.9 4.9 7 7M17 17l2.1 2.1M4.9 19.1 7 17M17 7l2.1-2.1" />
    </Glyph>
  ),
};

/** A plain list: the items stack 4 px apart in a 216 px column, as in Figma's sidebar. */
const LIST: CSSProperties = {
  display: 'grid',
  gap: 4,
  inlineSize: 216,
  margin: 0,
  padding: 0,
  listStyle: 'none',
};

export function NavItemExample(): JSX.Element {
  return (
    <nav aria-label="Main">
      <ul style={LIST}>
        <li>
          <NavItem href="/" icon={ICONS.home}>
            Home
          </NavItem>
        </li>
        <li>
          <NavItem href="/orders" icon={ICONS.orders} count={12} current>
            Orders
          </NavItem>
        </li>
        <li>
          <NavItem href="/products" icon={ICONS.products}>
            Products
          </NavItem>
        </li>
        <li>
          <NavItem href="/customers" icon={ICONS.customers} count={3}>
            Customers
          </NavItem>
        </li>
      </ul>
    </nav>
  );
}

Router

A router's link goes in through render; a disabled item is not a Tab stop and goes nowhere.

import type { ComponentPropsWithRef, CSSProperties, JSX, ReactNode } from 'react';
import { NavItem } from '@nizivo/react/nav-item';

/** Stands in for a router link (Next `Link`, React Router `Link`). */
function Link({ children, ...props }: ComponentPropsWithRef<'a'>): JSX.Element {
  return <a {...props}>{children}</a>;
}

/** Stands in for an `@nizivo/icons` glyph (20 px Lucide strokes, sized by the slot). */
function Glyph({ children }: { children: ReactNode }): JSX.Element {
  return (
    <svg
      viewBox="0 0 24 24"
      width="1em"
      height="1em"
      fill="none"
      stroke="currentColor"
      strokeWidth={1.92}
      strokeLinecap="round"
      strokeLinejoin="round"
    >
      {children}
    </svg>
  );
}

const ICONS = {
  home: (
    <Glyph>
      <path d="M3 10.5 12 3l9 7.5V21H3z" />
      <path d="M9 21v-7h6v7" />
    </Glyph>
  ),
  orders: (
    <Glyph>
      <path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z" />
      <path d="M3 6h18M16 10a4 4 0 0 1-8 0" />
    </Glyph>
  ),
  products: (
    <Glyph>
      <path d="m21 8-9-5-9 5v8l9 5 9-5z" />
      <path d="m3 8 9 5 9-5M12 13v8" />
    </Glyph>
  ),
  customers: (
    <Glyph>
      <circle cx="9" cy="8" r="4" />
      <path d="M2 21a7 7 0 0 1 14 0M16 4a4 4 0 0 1 0 8M22 21a7 7 0 0 0-4-6.3" />
    </Glyph>
  ),
  settings: (
    <Glyph>
      <circle cx="12" cy="12" r="3" />
      <path d="M12 2v3M12 19v3M2 12h3M19 12h3M4.9 4.9 7 7M17 17l2.1 2.1M4.9 19.1 7 17M17 7l2.1-2.1" />
    </Glyph>
  ),
};

/** A plain list: the items stack 4 px apart in a 216 px column, as in Figma's sidebar. */
const LIST: CSSProperties = {
  display: 'grid',
  gap: 4,
  inlineSize: 216,
  margin: 0,
  padding: 0,
  listStyle: 'none',
};

export function NavItemRouterExample(): JSX.Element {
  return (
    <nav aria-label="Settings">
      <ul style={LIST}>
        <li>
          <NavItem render={<Link href="/settings/general" />} icon={ICONS.settings} current>
            General
          </NavItem>
        </li>
        <li>
          <NavItem render={<Link href="/settings/payments" />} icon={ICONS.orders}>
            Payments
          </NavItem>
        </li>
        <li>
          <NavItem icon={ICONS.customers} disabled>
            Staff accounts
          </NavItem>
        </li>
      </ul>
    </nav>
  );
}

Collapsed

The icon rail: a Sidebar keeps it with collapsed (and draws it from 640 px to 1280 px on its own). The labels and counts stay in the DOM as the links' names, and a count shows as a dot. In a Sidebar, use SidebarItem: the same link, with a Tooltip that shows its label in the rail.

import type { JSX, ReactNode } from 'react';
import { Sidebar, SidebarItem } from '@nizivo/react/sidebar';

/** Stands in for an `@nizivo/icons` glyph (20 px Lucide strokes, sized by the slot). */
function Glyph({ children }: { children: ReactNode }): JSX.Element {
  return (
    <svg
      viewBox="0 0 24 24"
      width="1em"
      height="1em"
      fill="none"
      stroke="currentColor"
      strokeWidth={1.92}
      strokeLinecap="round"
      strokeLinejoin="round"
    >
      {children}
    </svg>
  );
}

const ICONS = {
  home: (
    <Glyph>
      <path d="M3 10.5 12 3l9 7.5V21H3z" />
      <path d="M9 21v-7h6v7" />
    </Glyph>
  ),
  orders: (
    <Glyph>
      <path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z" />
      <path d="M3 6h18M16 10a4 4 0 0 1-8 0" />
    </Glyph>
  ),
  products: (
    <Glyph>
      <path d="m21 8-9-5-9 5v8l9 5 9-5z" />
      <path d="m3 8 9 5 9-5M12 13v8" />
    </Glyph>
  ),
  customers: (
    <Glyph>
      <circle cx="9" cy="8" r="4" />
      <path d="M2 21a7 7 0 0 1 14 0M16 4a4 4 0 0 1 0 8M22 21a7 7 0 0 0-4-6.3" />
    </Glyph>
  ),
  settings: (
    <Glyph>
      <circle cx="12" cy="12" r="3" />
      <path d="M12 2v3M12 19v3M2 12h3M19 12h3M4.9 4.9 7 7M17 17l2.1 2.1M4.9 19.1 7 17M17 7l2.1-2.1" />
    </Glyph>
  ),
};

export function NavItemCollapsedExample(): JSX.Element {
  return (
    <Sidebar.Root collapsed aria-label="Main, collapsed" style={{ blockSize: 'auto' }}>
      <Sidebar.Content>
        <SidebarItem href="/" icon={ICONS.home}>
          Home
        </SidebarItem>
        <SidebarItem href="/orders" icon={ICONS.orders} count={12} current>
          Orders
        </SidebarItem>
        <SidebarItem href="/customers" icon={ICONS.customers} count={3}>
          Customers
        </SidebarItem>
      </Sidebar.Content>
    </Sidebar.Root>
  );
}

Props

Server component Renders data-slot="nav-item" with the class nz-nav-item. Props: NavItemProps. Source: src/nav-item/nav-item.tsx.

NameTypeDefaultDescription
childrenrequiredReactNode–

The label (Figma's Label). It stays the accessible name in the collapsed rail.

countReactNode–

A count after the label, such as the number of open orders (Figma's Count). Absent, or null, shows none. It is part of the link's text, so it is announced with the label.

currentbooleanfalse

The destination being shown: sets aria-current="page" and draws the filled state (Figma State=Active). The page knows its own route, so it says which item is current.

disabledbooleanfalse

Not available: renders a <span role="link" aria-disabled="true"> with no href, so it is out of the Tab order and cannot be followed. render is ignored.

iconReactNode–

A decorative glyph before the label (20 px, Figma's Icon slot); it is aria-hidden.

renderRenderProp<NavItemState>–

The router's link, for example render={<Link href="/orders" />} (Next.js, React Router, TanStack Router). It must render an <a>.

  • Also takes every attribute of <a> except children. (Omit<ComponentPropsWithRef<"a">, "children">)

render takes an element, or, in client code, a function of the props and this state (NavItemState):

NameTypeDefaultDescription
currentrequiredboolean–
disabledrequiredboolean–

Other exports

NavItemProps type

import type { NavItemProps } from '@nizivo/react/nav-item'

The props of NavItem, listed above.

NavItemState type

import type { NavItemState } from '@nizivo/react/nav-item'

The render state of NavItem, listed above.

Data attributes and slots

Style parts by class (.nz-<slot>) and state by data attribute. Attribute meanings come from contracts/vocabulary.json.

SlotRendered byData attributes
nav-item
.nz-nav-item
NavItem
root (takes render); interactive, focus: self; forced colours: highlight on [aria-current='page']
none
nav-item-count
.nz-nav-item-count
NavItem
inner element (CSS only)
none
nav-item-icon
.nz-nav-item-icon
NavItem
inner element (CSS only)
none
nav-item-label
.nz-nav-item-label
NavItem
inner element (CSS only)
none

CSS hooks

Set a hook on the component's class or any ancestor to restyle it without specificity games; leave it unset for the default shown.

HookDefaultRead as
--nz-nav-item-bgtransparent
  • background on .nz-nav-item
--nz-nav-item-bg-current--nz-bg-brand-subtle
  • background on .nz-nav-item[aria-current="page"]
--nz-nav-item-bg-hover--nz-bg-muted
  • background on .nz-nav-item:hover:not([aria-current="page"], [data-disabled]) @media (hover: hover)
--nz-nav-item-count-bg--nz-bg-muted
  • background on .nz-nav-item-count
--nz-nav-item-count-bg-current--nz-bg-brand
  • background on .nz-nav-item:has( > .nz-nav-item-count) > .nz-nav-item-icon:after @container nz-sidebar (inline-size < 10rem)
  • background on .nz-nav-item[aria-current="page"] > .nz-nav-item-count
--nz-nav-item-fg--nz-text-secondary
  • color on .nz-nav-item
--nz-nav-item-fg-current--nz-text-brand
  • color on .nz-nav-item[aria-current="page"]
--nz-nav-item-gap--nz-space-3
  • gap on .nz-nav-item
--nz-nav-item-height--nz-size-control-md
  • block-size on .nz-nav-item
  • inline-size on .nz-nav-item @container nz-sidebar (inline-size < 10rem)
--nz-nav-item-icon-fg--nz-icon-secondary
  • color on .nz-nav-item-icon
--nz-nav-item-radius--nz-radius-md
  • border-radius on .nz-nav-item

Layers: nz.components. Classes: .nz-nav-item, .nz-nav-item-count, .nz-nav-item-icon, .nz-nav-item-label.

Tokens this stylesheet reads (26)

--nz-bg-brand, --nz-bg-brand-subtle, --nz-bg-muted, --nz-bg-pressed, --nz-duration-fast, --nz-easing-standard, --nz-font-label-m, --nz-font-label-m-strong, --nz-font-label-m-strong-font-weight, --nz-icon-brand, --nz-icon-secondary, --nz-radius-full, --nz-radius-md, --nz-size-control-md, --nz-size-icon-md, --nz-space-0-5, --nz-space-1-5, --nz-space-2, --nz-space-3, --nz-text-brand, --nz-text-disabled, --nz-text-on-brand, --nz-text-primary, --nz-text-secondary, --nz-tracking-label-m, --nz-tracking-label-m-strong

Accessibility

These parts are interactive. Each gets the focus outline and touch-action rule from @nizivo/react/css/base.css (the nz.base layer), so the floor survives a restyle:

  • nav-item: draws the focus outline itself on keyboard focus.

In forced-colours mode these states keep a visible system colour: nav-item[aria-current='page'] (highlight).

Keyboard

KeysAction
Tab, Shift+TabMoves between the links, as on any link. A disabled item is not a Tab stop.
EnterFollows the focused link.

Source: native <a> elements (nav-item.tsx, no client code). The browser lane (test/browser/nav-item.spec.tsx) checks Tab through the links in order, a disabled item not being a stop, the focus outline, the fixed 40 px row and the collapsed rail.

Figma

Node ids from design/component-map.json, which nz gen writes from component.json. The links open the Nizivo Figma library, which may ask you to sign in.

On this page