Skip to content
Nizivo UI

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.

Server component with client leaf SidebarItem

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 own aria-label).
  • Sidebar.Header: the store, on a card. mark draws Figma's 32 px Store mark (an initial, or a logo image that fills it), title the store's name and description the line under it. Pass render={<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 (href or render, 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.

NameTypeDefaultDescription
childrenrequiredReactNode–

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

From NavItemProps.

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.

From NavItemProps.

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.

From NavItemProps.

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.

From NavItemProps.

iconReactNode–

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

From NavItemProps.

renderRenderProp<NavItemState>–

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

From NavItemProps.

  • 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–

Parts

Use the parts through the namespace (Sidebar.Root) or by their flat names (SidebarRoot); both work from Server Components.

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.

NameTypeDefaultDescription
collapsedboolean–

Overrides the width the viewport gives: true keeps the icon rail and false the full sidebar at every width from 640 px, for an app with its own collapse control. Below 640 px the Sidebar is not shown either way. Absent, it follows the viewport.

labelsPartial<SidebarLabels>–
renderRenderProp<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.

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.

NameTypeDefaultDescription
descriptionReactNode–

A line under the name, such as the store's domain.

markReactNode–

The store's mark, for example a square Avatar (Figma's 32 px Store mark). The rail keeps it.

renderRenderProp<Empty>–

A link to the store's home or a store switcher's trigger: render={<a href="/" />}.

titleReactNode–

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> except title. (Omit<ComponentPropsWithRef<"div">, "title">)

render takes an element, or, in client code, a function of the props and no state.

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.

NameTypeDefaultDescription
renderRenderProp<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.

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.

NameTypeDefaultDescription
renderRenderProp<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

SidebarContentProps type

import type { SidebarContentProps } from '@nizivo/react/sidebar'

The props of Sidebar.Content, listed above.

SidebarFooterProps type

import type { SidebarFooterProps } from '@nizivo/react/sidebar'

The props of Sidebar.Footer, listed above.

SidebarHeaderProps type

import type { SidebarHeaderProps } from '@nizivo/react/sidebar'

The props of Sidebar.Header, listed above.

SidebarItemProps type

import type { SidebarItemProps } from '@nizivo/react/sidebar'

type SidebarItemProps = NavItemProps

The props of SidebarItem, listed above.

SidebarLabels type

import type { SidebarLabels } from '@nizivo/react/sidebar'

Strings this part renders (§16.4).

NameTypeDefaultDescription
navrequiredstring'Primary'

The navigation landmark's name.

SidebarRootProps type

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

SlotRendered byData attributes
sidebar
.nz-sidebar
Sidebar.Root
root (takes render)
  • data-collapsed (present or absent) ← collapsed
    Kept as the icon rail by its collapsed prop
  • data-expanded (present or absent)
    Kept at full width by collapsed={false}
sidebar-content
.nz-sidebar-content
Sidebar.Content
root (takes render)
none
sidebar-footer
.nz-sidebar-footer
Sidebar.Footer
root (takes render)
none
sidebar-header
.nz-sidebar-header
Sidebar.Header
root (takes render)
none
sidebar-header-description
.nz-sidebar-header-description
Sidebar.Header
inner element (CSS only)
none
sidebar-header-mark
.nz-sidebar-header-mark
Sidebar.Header
inner element (CSS only)
none
sidebar-header-text
.nz-sidebar-header-text
Sidebar.Header
inner element (CSS only)
none
sidebar-header-title
.nz-sidebar-header-title
Sidebar.Header
inner element (CSS only)
none
sidebar-item
.nz-sidebar-item
SidebarItem
inner 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.

HookDefaultRead as
--nz-sidebar-bg--nz-bg-canvas
  • background on .nz-sidebar
--nz-sidebar-block-size100dvh
  • block-size on .nz-sidebar
--nz-sidebar-border--nz-border-subtle
  • border-inline-end on .nz-sidebar
--nz-sidebar-mark-bg--nz-bg-brand
  • background on .nz-sidebar-header-mark
--nz-sidebar-width
  • default: var(--nz-layout-sidebar)
  • data-collapsed: 4.5rem
  • data-expanded: 16.5rem
  • inline-size on .nz-sidebar

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

KeysAction
Tab, Shift+TabMoves between the links, as on any link. In the rail, a focused link opens a Tooltip with its label at once.
EscapeIn the rail, closes the open Tooltip; focus stays on the link.
EnterFollows 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.

On this page