Sidebar
The app's main navigation on screens 640 px and wider, with the store at the top, the destinations, and Settings at the foot. It is 264 px wide from 1280 px and folds to a 72 px icon rail below that, where each destination's Tooltip names it.
Import
import { Sidebar, SidebarItem } from '@nizivo/react/sidebar';
// Once per app (Getting started), after
// @nizivo/tokens/tokens.css and @nizivo/react/css/base.css and @nizivo/react/css/nav-item.css and @nizivo/react/css/tooltip.css
import '@nizivo/react/css/sidebar.css';Static or interactive
Split. Sidebar.Root, Header, Content and Footer are server parts with no client
JavaScript; the width follows the viewport through CSS, so the server and the browser render the
same markup. SidebarItem is the client part: NavItem's link with the rail's Tooltip, which brings
the Tooltip's code (about 16.5 KB gzip, shared with any IconButton's tooltip on the page). 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.
Parts
Sidebar.Root: a<nav>landmark named "Primary" (labels.nav, or your ownaria-label).Sidebar.Header: the store, on a card.markdraws Figma's 32 px Store mark (an initial, or a logo image that fills it),titlethe store's name anddescriptionthe line under it. Passrender={<a href="/" />}to make it a link home, or a store switcher's trigger.Sidebar.Content: the destinations, 4 px apart.Sidebar.Footer: pushed to the bottom, for Settings.SidebarItem: a destination. It takes NavItem's props (hreforrender,current,icon,count,disabled) and renders the same link, as the trigger of a Tooltip with its label.
Widths
The width is the layout/sidebar token: 264 px from 1280 px (--nz-lg), the 72 px icon rail from
640 px, and below 640 px the Sidebar is not shown, because the MobileTabBar is the navigation there
(M49; Figma's frames are drawn at 248 and 76 px). The 1280 px threshold is provisional until design
confirms it. An app with its own collapse control passes collapsed: true keeps the rail and
false the full width at every width from 640 px. Store the choice where the server can read it, a
cookie for example, so the first paint already has the right width.
The rail
In the rail each item is a 40 px square around its icon, and the header keeps only its mark. The
labels and the counts stay in the DOM as the links' names ("Orders 12"), and a count shows as a dot.
Each SidebarItem opens a Tooltip with its label at the rail's end side, at once on keyboard focus
and after the delay on hover; Escape closes it, and the pointer can move onto it (M52: "keep tooltip,
because accessibility is important"). The Tooltip repeats the link's name, so the link is not
described by it. Expanded, the label is on screen and no Tooltip opens. Give every item an icon.
The rail is a size container (nz-sidebar), so the items fold whenever the Sidebar is rail-narrow,
whatever made it so. A plain NavItem folds the same way but has no Tooltip.
Height and scrolling
The Sidebar sticks to the top and is as tall as the viewport (--nz-sidebar-block-size); a long list
scrolls inside it.
Hooks: --nz-sidebar-width, --nz-sidebar-block-size, --nz-sidebar-bg, --nz-sidebar-border,
--nz-sidebar-mark-bg.
Examples
Basic
Figma's Sidebar, expanded: the store, the destinations (SidebarItems) and Settings at the foot. collapsed={false} keeps it 264 px wide here; in an app it is 264 px from 1280 px and the icon rail from 640 px. (The examples set blockSize; in a shell it is the viewport's height.)
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>
),
analytics: (
<Glyph>
<path d="M3 3v18h18M8 17v-5M13 17V8M18 17v-9" />
</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>
),
};
function Destinations(): JSX.Element {
return (
<>
<Sidebar.Header mark="M" title="Moss & Kiln" description="mossandkiln.com" />
<Sidebar.Content>
<SidebarItem href="/" icon={ICONS.home}>
Home
</SidebarItem>
<SidebarItem href="/orders" icon={ICONS.orders} count={12} current>
Orders
</SidebarItem>
<SidebarItem href="/products" icon={ICONS.products}>
Products
</SidebarItem>
<SidebarItem href="/customers" icon={ICONS.customers}>
Customers
</SidebarItem>
<SidebarItem href="/analytics" icon={ICONS.analytics}>
Analytics
</SidebarItem>
</Sidebar.Content>
<Sidebar.Footer>
<SidebarItem href="/settings" icon={ICONS.settings}>
Settings
</SidebarItem>
</Sidebar.Footer>
</>
);
}
export function SidebarExample(): JSX.Element {
return (
<Sidebar.Root collapsed={false} style={{ blockSize: 480 }}>
<Destinations />
</Sidebar.Root>
);
}
Rail
The icon rail (Expanded=False): only the store's mark and the icons. The labels stay the links' names, a count shows as a dot, and each SidebarItem shows its label in a Tooltip on hover and keyboard focus.
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>
),
analytics: (
<Glyph>
<path d="M3 3v18h18M8 17v-5M13 17V8M18 17v-9" />
</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>
),
};
function Destinations(): JSX.Element {
return (
<>
<Sidebar.Header mark="M" title="Moss & Kiln" description="mossandkiln.com" />
<Sidebar.Content>
<SidebarItem href="/" icon={ICONS.home}>
Home
</SidebarItem>
<SidebarItem href="/orders" icon={ICONS.orders} count={12} current>
Orders
</SidebarItem>
<SidebarItem href="/products" icon={ICONS.products}>
Products
</SidebarItem>
<SidebarItem href="/customers" icon={ICONS.customers}>
Customers
</SidebarItem>
<SidebarItem href="/analytics" icon={ICONS.analytics}>
Analytics
</SidebarItem>
</Sidebar.Content>
<Sidebar.Footer>
<SidebarItem href="/settings" icon={ICONS.settings}>
Settings
</SidebarItem>
</Sidebar.Footer>
</>
);
}
export function SidebarRailExample(): JSX.Element {
return (
<Sidebar.Root collapsed style={{ blockSize: 480 }}>
<Destinations />
</Sidebar.Root>
);
}
Props
A NavItem for a Sidebar: the same link, with a Tooltip that names it while the Sidebar is the icon rail (hover after the delay, keyboard focus at once; Escape closes it).
Client component Renders no element of its own. Props: SidebarItemProps. Source: src/sidebar/sidebar-item.client.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
childrenrequired | ReactNode | – | The label (Figma's Label). It stays the accessible name in the collapsed rail. From |
count | ReactNode | – | A count after the label, such as the number of open orders (Figma's Count). Absent, or From |
current | boolean | false | The destination being shown: sets From |
disabled | boolean | false | Not available: renders a From |
icon | ReactNode | – | A decorative glyph before the label (20 px, Figma's Icon slot); it is From |
render | RenderProp<NavItemState> | – | The router's link, for example From |
- 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 | – |
Parts
Use the parts through the namespace (Sidebar.Root) or by their flat names (SidebarRoot); both work from Server Components.
Sidebar.Root
The <nav> landmark around everything in the sidebar.
Server component Renders data-slot="sidebar" with the class nz-sidebar. Props: SidebarRootProps. Source: src/sidebar/sidebar.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
collapsed | boolean | – | Overrides the width the viewport gives: |
labels | Partial<SidebarLabels> | – | |
render | RenderProp<Empty> | – |
- Also takes every attribute of
<nav>. (ComponentPropsWithRef<"nav">)
render takes an element, or, in client code, a function of the props and no state.
Sidebar.Header
The store at the top: its mark, name and a line under it, on a card (Figma's Store Switcher).
Server component Renders data-slot="sidebar-header" with the class nz-sidebar-header. Props: SidebarHeaderProps. Source: src/sidebar/sidebar.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
description | ReactNode | – | A line under the name, such as the store's domain. |
mark | ReactNode | – | The store's mark, for example a square |
render | RenderProp<Empty> | – | A link to the store's home or a store switcher's trigger: |
title | ReactNode | – | The store's name (Figma's Store). In the rail it leaves the layout and stays in the DOM. |
- Also takes every attribute of
<div>excepttitle. (Omit<ComponentPropsWithRef<"div">, "title">)
render takes an element, or, in client code, a function of the props and no state.
Sidebar.Content
The destinations, NavItems 4 px apart.
Server component Renders data-slot="sidebar-content" with the class nz-sidebar-content. Props: SidebarContentProps. Source: src/sidebar/sidebar.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
render | RenderProp<Empty> | – |
- Also takes every attribute of
<div>. (ComponentPropsWithRef<"div">)
render takes an element, or, in client code, a function of the props and no state.
Sidebar.Footer
Pushed to the bottom, for Settings (Figma's Spacer frame becomes an auto margin).
Server component Renders data-slot="sidebar-footer" with the class nz-sidebar-footer. Props: SidebarFooterProps. Source: src/sidebar/sidebar.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
render | RenderProp<Empty> | – |
- Also takes every attribute of
<div>. (ComponentPropsWithRef<"div">)
render takes an element, or, in client code, a function of the props and no state.
Other exports
SidebarContentPropstypeimport type { SidebarContentProps } from '@nizivo/react/sidebar'The props of
Sidebar.Content, listed above.SidebarFooterPropstypeimport type { SidebarFooterProps } from '@nizivo/react/sidebar'The props of
Sidebar.Footer, listed above.SidebarHeaderPropstypeimport type { SidebarHeaderProps } from '@nizivo/react/sidebar'The props of
Sidebar.Header, listed above.SidebarItemPropstypeimport type { SidebarItemProps } from '@nizivo/react/sidebar'type SidebarItemProps = NavItemPropsThe props of
SidebarItem, listed above.SidebarLabelstypeimport type { SidebarLabels } from '@nizivo/react/sidebar'Strings this part renders (§16.4).
Name Type Default Description navrequiredstring'Primary'The navigation landmark's name.
SidebarRootPropstypeimport type { SidebarRootProps } from '@nizivo/react/sidebar'The props of
Sidebar.Root, 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 |
|---|---|---|
sidebar.nz-sidebar | Sidebar.Rootroot (takes render) |
|
sidebar-content.nz-sidebar-content | Sidebar.Contentroot (takes render) | none |
sidebar-footer.nz-sidebar-footer | Sidebar.Footerroot (takes render) | none |
sidebar-header.nz-sidebar-header | Sidebar.Headerroot (takes render) | none |
sidebar-header-description.nz-sidebar-header-description | Sidebar.Headerinner element (CSS only) | none |
sidebar-header-mark.nz-sidebar-header-mark | Sidebar.Headerinner element (CSS only) | none |
sidebar-header-text.nz-sidebar-header-text | Sidebar.Headerinner element (CSS only) | none |
sidebar-header-title.nz-sidebar-header-title | Sidebar.Headerinner element (CSS only) | none |
sidebar-item.nz-sidebar-item | SidebarIteminner element (CSS only); interactive, focus: self | 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-sidebar-bg | --nz-bg-canvas |
|
--nz-sidebar-block-size | 100dvh |
|
--nz-sidebar-border | --nz-border-subtle |
|
--nz-sidebar-mark-bg | --nz-bg-brand |
|
--nz-sidebar-width |
|
|
Layers: nz.components. Classes: .nz-sidebar, .nz-sidebar-content, .nz-sidebar-footer, .nz-sidebar-header, .nz-sidebar-header-description, .nz-sidebar-header-mark, .nz-sidebar-header-text, .nz-sidebar-header-title.
Tokens this stylesheet reads (22)
--nz-bg-brand, --nz-bg-canvas, --nz-bg-muted, --nz-bg-surface, --nz-border-subtle, --nz-border-width-default, --nz-font-body-m, --nz-font-label-m-strong, --nz-layout-sidebar, --nz-radius-lg, --nz-radius-sm, --nz-space-1, --nz-space-2, --nz-space-3, --nz-space-4, --nz-space-8, --nz-text-on-brand, --nz-text-primary, --nz-text-secondary, --nz-tracking-body-m, --nz-tracking-label-m-strong, --nz-z-chrome
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:
sidebar-item: draws the focus outline itself on keyboard focus.
Keyboard
| Keys | Action |
|---|---|
| Tab, Shift+Tab | Moves between the links, as on any link. In the rail, a focused link opens a Tooltip with its label at once. |
| Escape | In the rail, closes the open Tooltip; focus stays on the link. |
| Enter | Follows the focused link. |
Source: native <a> elements (NavItem); in the rail, each SidebarItem is the trigger of the Tooltip (react-aria-components 1.22.1 TooltipTrigger, through the Tooltip's engine adapter). The browser lane (test/browser/app-shell.spec.tsx, test/browser/nav-item.spec.tsx) checks the widths at 1280 and 960 px, the rail's 40 px squares, the Tooltip on keyboard focus and hover in the rail, Escape closing it, none expanded, and no aria-describedby on the link, in Chromium, Firefox and WebKit.
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:14732 - Ghost masters: Figma node
755:869
Separator
An <hr>: a 1 px border/subtle line, Figma's Divider (7370:95). Decorative by default; spacing comes first, a line only when spacing is not enough (the Figma doc).
Skeleton
One placeholder block, always hidden from assistive technology. The region it stands in for sets aria-busy and carries visually hidden "Loading …" text (§16.5). There is no style prop path: size it with a class, or with --nz-skeleton-width, --nz-skeleton-height and --nz-skeleton-radius.