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.
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.
| Name | Type | Default | Description |
|---|---|---|---|
childrenrequired | ReactNode | – | The label (Figma's Label). It stays the accessible name in the collapsed rail. |
count | ReactNode | – | A count after the label, such as the number of open orders (Figma's Count). Absent, or |
current | boolean | false | The destination being shown: sets |
disabled | boolean | false | Not available: renders a |
icon | ReactNode | – | A decorative glyph before the label (20 px, Figma's Icon slot); it is |
render | RenderProp<NavItemState> | – | The router's link, for example |
- Also takes every attribute of
<a>exceptchildren. (Omit<ComponentPropsWithRef<"a">, "children">)
render takes an element, or, in client code, a function of the props and this state (NavItemState):
| Name | Type | Default | Description |
|---|---|---|---|
currentrequired | boolean | – | |
disabledrequired | boolean | – |
Other exports
NavItemPropstypeimport type { NavItemProps } from '@nizivo/react/nav-item'The props of
NavItem, listed above.NavItemStatetypeimport 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.
| Slot | Rendered by | Data attributes |
|---|---|---|
nav-item.nz-nav-item | NavItemroot (takes render); interactive, focus: self; forced colours: highlight on [aria-current='page'] | none |
nav-item-count.nz-nav-item-count | NavIteminner element (CSS only) | none |
nav-item-icon.nz-nav-item-icon | NavIteminner element (CSS only) | none |
nav-item-label.nz-nav-item-label | NavIteminner 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.
| Hook | Default | Read as |
|---|---|---|
--nz-nav-item-bg | transparent |
|
--nz-nav-item-bg-current | --nz-bg-brand-subtle |
|
--nz-nav-item-bg-hover | --nz-bg-muted |
|
--nz-nav-item-count-bg | --nz-bg-muted |
|
--nz-nav-item-count-bg-current | --nz-bg-brand |
|
--nz-nav-item-fg | --nz-text-secondary |
|
--nz-nav-item-fg-current | --nz-text-brand |
|
--nz-nav-item-gap | --nz-space-3 |
|
--nz-nav-item-height | --nz-size-control-md |
|
--nz-nav-item-icon-fg | --nz-icon-secondary |
|
--nz-nav-item-radius | --nz-radius-md |
|
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
| Keys | Action |
|---|---|
| Tab, Shift+Tab | Moves between the links, as on any link. A disabled item is not a Tab stop. |
| Enter | Follows 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.
- Canonical: Figma node
7275:14700 - Ghost masters: Figma node
755:658
NativeSelect
The browser's own select, styled like the Select trigger. Server-safe with no client code; use it where the native picker is the better choice, such as on phones or in Server Component forms.
NumberField
A number in the reader's locale, a price, a percent or a unit, in Input's box. It formats on blur, parses what is typed in the locale, clamps to min and max, and steps with the arrow keys; step buttons are optional. A form gets the plain number.