Tabs
In-page views of one subject, one panel at a time, as WAI-ARIA tabs. Only the selected panel is rendered, on the server too; keepMounted keeps a panel's state while it is hidden. When each view is a URL, use TabNav; to pick a range or a value, use SegmentedControl.
Import
import { Tabs } from '@nizivo/react/tabs';
// Once per app (Getting started), after
// @nizivo/tokens/tokens.css and @nizivo/react/css/base.css
import '@nizivo/react/css/tabs.css';Static or interactive
Client. It always ships React Aria's tabs code (+38,812 B gzip under Turbopack for one Tabs on the Next.js fixture). When every view is a URL, TabNav does the job with no client code. How to choose.
Direction
The strip's direction comes from the NizivoConfig scope: under a right-to-left locale (Arabic,
Hebrew, Persian and so on) the layout and the arrow keys follow it, and ArrowLeft moves to the next tab.
A strip whose direction differs from its locale's sets dir on Tabs.Root, which wins over the
scope:
<NizivoConfig locale="de-DE">
{/* a strip of Arabic labels inside a German page */}
<Tabs.Root dir="rtl" defaultValue="overview">
…
</Tabs.Root>
</NizivoConfig>dir takes 'ltr' or 'rtl' and is rendered as the root's dir attribute. Without it, nothing is
rendered and the nearest Tabs.Root around the strip that sets dir decides, so a strip nested in
its panels keys the way CSS lays it out; with none, the locale decides. Nothing reads the page's DOM,
so a dir attribute on any other ancestor does not turn the arrows, and 'auto' is not accepted.
The override covers the tab list's keys only: the tab labels and the panels keep the scope's locale
(a number formatted in a label stays in it), and a Menu, Select or Tooltip inside a panel takes its
direction from NizivoConfig.
Examples
Basic
In-page views of one list. Arrow keys move between tabs and show each panel as they land on it; Tab moves on into the panel. Only the selected panel is rendered, on the server too.
Every order from the last 90 days.
import type { JSX } from 'react';
import { Text } from '@nizivo/react/text';
import { Tabs } from '@nizivo/react/tabs';
const STATUSES = [
['all', 'All orders', 'Every order from the last 90 days.'],
['unfulfilled', 'Unfulfilled', 'Paid orders that still need to ship.'],
['unpaid', 'Unpaid', 'Orders waiting for a payment.'],
['open', 'Open', 'Orders with a pending return or exchange.'],
['archived', 'Archived', 'Orders you closed.'],
] as const;
export function TabsExample(): JSX.Element {
return (
<Tabs.Root defaultValue="all">
<Tabs.List aria-label="Order status">
{STATUSES.map(([value, label]) => (
<Tabs.Tab key={value} value={value}>
{label}
</Tabs.Tab>
))}
</Tabs.List>
{STATUSES.map(([value, , text]) => (
<Tabs.Panel key={value} value={value}>
<Text tone="secondary">{text}</Text>
</Tabs.Panel>
))}
</Tabs.Root>
);
}
States
A glyph before the label and a count after it; one tab is disabled and is skipped by the arrows.
12 orders are ready to pack.
import type { JSX } from 'react';
import { Text } from '@nizivo/react/text';
import { Tabs } from '@nizivo/react/tabs';
/** Stand-ins for `@nizivo/icons` glyphs (16 px Lucide strokes). */
function Package(): JSX.Element {
return (
<svg
viewBox="0 0 24 24"
width="1em"
height="1em"
fill="none"
stroke="currentColor"
strokeWidth={1.92}
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M21 8 12 3 3 8v8l9 5 9-5zM3 8l9 5 9-5M12 13v8" />
</svg>
);
}
function Truck(): JSX.Element {
return (
<svg
viewBox="0 0 24 24"
width="1em"
height="1em"
fill="none"
stroke="currentColor"
strokeWidth={1.92}
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M2 6h12v10H2zM14 9h4l4 4v3h-8" />
<circle cx="6" cy="18" r="2" />
<circle cx="18" cy="18" r="2" />
</svg>
);
}
export function TabsStatesExample(): JSX.Element {
return (
<Tabs.Root defaultValue="to-pack">
<Tabs.List aria-label="Fulfilment">
<Tabs.Tab value="to-pack" icon={<Package />} count={12}>
To pack
</Tabs.Tab>
<Tabs.Tab value="shipped" icon={<Truck />} count={48}>
Shipped
</Tabs.Tab>
<Tabs.Tab value="returns" count={0} disabled>
Returns
</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="to-pack">
<Text tone="secondary">12 orders are ready to pack.</Text>
</Tabs.Panel>
<Tabs.Panel value="shipped">
<Text tone="secondary">48 orders shipped this week.</Text>
</Tabs.Panel>
<Tabs.Panel value="returns">
<Text tone="secondary">No returns.</Text>
</Tabs.Panel>
</Tabs.Root>
);
}
Manual
activationMode="manual": arrow keys only move focus and Enter or Space selects, for panels that are slow to show. keepMounted keeps a panel's state (here, a draft) while another tab is shown.
import type { JSX } from 'react';
import { Text } from '@nizivo/react/text';
import { Textarea } from '@nizivo/react/textarea';
import { Tabs } from '@nizivo/react/tabs';
export function TabsManualExample(): JSX.Element {
return (
<Tabs.Root defaultValue="write" activationMode="manual">
<Tabs.List aria-label="Note">
<Tabs.Tab value="write">Write</Tabs.Tab>
<Tabs.Tab value="preview">Preview</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="write" keepMounted>
<Textarea aria-label="Note text" defaultValue="Ship the blue mugs first." />
</Tabs.Panel>
<Tabs.Panel value="preview">
<Text tone="secondary">The preview renders when this tab is selected.</Text>
</Tabs.Panel>
</Tabs.Root>
);
}
Parts
Use the parts through the namespace (Tabs.Root) or by their flat names (TabsRoot); both work from Server Components.
Tabs.Root
Client component Renders data-slot="tabs" with the class nz-tabs. Props: TabsRootProps. Source: src/tabs/tabs.client.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
activationMode | TabsActivationMode"automatic""manual" | 'automatic' | |
childrenrequired | ReactNode | – | One |
defaultValue | string | – | The tab selected at first (uncontrolled). Defaults to the first tab that is not disabled. |
dir | "ltr" | "rtl" | – | The strip's reading direction (M26), rendered as the root's |
disabled | boolean | – | Disables every tab. The selected panel still shows. |
onValueChange | (value: string, details: ChangeDetails<TabsValueReason>) => void | – | Called when the user selects a tab. |
render | RenderProp<TabsRootState> | – | Must render a |
value | string | – | Controlled: the selected tab's |
- Also takes every attribute of
<div>exceptchildren,defaultValue,dir,id,onChange. (Omit<ComponentPropsWithRef<"div">, "children" | "defaultValue" | "onChange" | "id" | "dir">)
render takes an element, or, in client code, a function of the props and this state (TabsRootState):
| Name | Type | Default | Description |
|---|---|---|---|
disabledrequired | boolean | – |
Tabs.List
Client component Renders data-slot="tabs-list" with the class nz-tabs-list. Props: TabsListProps. Source: src/tabs/tabs.client.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
childrenrequired | ReactNode | – |
|
render | RenderProp<TabsListState> | – | Must render a |
- Also takes every attribute of
<div>exceptchildren,id. (Omit<ComponentPropsWithRef<"div">, "children" | "id">)
render takes an element, or, in client code, a function of the props and this state (TabsListState):
| Name | Type | Default | Description |
|---|---|---|---|
disabledrequired | boolean | – |
Tabs.Tab
Client component Renders data-slot="tabs-tab" with the class nz-tabs-tab. Props: TabsTabProps. Source: src/tabs/tabs.client.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
childrenrequired | ReactNode | – | |
count | ReactNode | – | A count after the label, such as the number of orders in a status. |
disabled | boolean | – | |
icon | ReactNode | – | A decorative glyph before the label (16 px). |
render | RenderProp<TabsTabState> | – | Must render a |
valuerequired | string | – | Pairs the tab with the |
- Also takes every attribute of
<div>exceptchildren,id,onClick. (Omit<ComponentPropsWithRef<"div">, "children" | "id" | "onClick">)
render takes an element, or, in client code, a function of the props and this state (TabsTabState):
| Name | Type | Default | Description |
|---|---|---|---|
disabledrequired | boolean | – | |
focusVisiblerequired | boolean | – | |
selectedrequired | boolean | – |
Tabs.Panel
Client component Renders data-slot="tabs-panel" with the class nz-tabs-panel. Props: TabsPanelProps. Source: src/tabs/tabs.client.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
children | ReactNode | – | |
keepMounted | boolean | false | Keeps the panel mounted, |
render | RenderProp<TabsPanelState> | – | Must render a |
valuerequired | string | – | The |
- Also takes every attribute of
<div>exceptchildren,id. (Omit<ComponentPropsWithRef<"div">, "children" | "id">)
render takes an element, or, in client code, a function of the props and this state (TabsPanelState):
| Name | Type | Default | Description |
|---|---|---|---|
focusVisiblerequired | boolean | – | |
selectedrequired | boolean | – | The panel's tab is selected. False only on a |
Other exports
TabsActivationModetypeimport type { TabsActivationMode } from '@nizivo/react/tabs'When a tab is selected (WAI-ARIA APG tabs). -
automatic: an arrow key moves focus and selects the tab it lands on (the APG default, for panels that show without a noticeable delay); -manual: arrow keys only move focus; Enter or Space selects (for panels that are slow to show).type TabsActivationMode = "automatic" | "manual"TabsListPropstypeimport type { TabsListProps } from '@nizivo/react/tabs'The props of
Tabs.List, listed above.TabsListStatetypeimport type { TabsListState } from '@nizivo/react/tabs'The render state of
Tabs.List, listed above.TabsPanelPropstypeimport type { TabsPanelProps } from '@nizivo/react/tabs'The props of
Tabs.Panel, listed above.TabsPanelStatetypeimport type { TabsPanelState } from '@nizivo/react/tabs'The render state of
Tabs.Panel, listed above.TabsRootPropstypeimport type { TabsRootProps } from '@nizivo/react/tabs'The props of
Tabs.Root, listed above.TabsRootStatetypeimport type { TabsRootState } from '@nizivo/react/tabs'The render state of
Tabs.Root, listed above.TabsTabPropstypeimport type { TabsTabProps } from '@nizivo/react/tabs'The props of
Tabs.Tab, listed above.TabsTabStatetypeimport type { TabsTabState } from '@nizivo/react/tabs'The render state of
Tabs.Tab, listed above.TabsValueReasontypeimport type { TabsValueReason } from '@nizivo/react/tabs'Why the selected tab changed. React Aria reports no cause, so with it
details.reasonis always'unknown'(§9.3 rule 2); the names are the causes an engine that reports one would use.type TabsValueReason = "tab-press" | "keyboard"
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 |
|---|---|---|
tabs.nz-tabs | Tabs.Rootroot (takes render) |
|
tabs-count.nz-tabs-count | Tabs.Tabinner element (CSS only) | none |
tabs-icon.nz-tabs-icon | Tabs.Tabinner element (CSS only) | none |
tabs-label.nz-tabs-label | Tabs.Tabinner element (CSS only) | none |
tabs-list.nz-tabs-list | Tabs.Listroot (takes render) |
|
tabs-panel.nz-tabs-panel | Tabs.Panelroot (takes render) |
|
tabs-tab.nz-tabs-tab | Tabs.Tabroot (takes render); interactive, focus: self; forced colours: fill on [data-selected]::before |
|
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-tabs-border | --nz-border-subtle |
|
--nz-tabs-count-bg | --nz-bg-subtle |
|
--nz-tabs-fg | --nz-text-secondary |
|
--nz-tabs-fg-selected | --nz-text-primary |
|
--nz-tabs-gap | --nz-space-4 |
|
--nz-tabs-indicator | --nz-bg-brand |
|
Layers: nz.components. Classes: .nz-tabs, .nz-tabs-count, .nz-tabs-icon, .nz-tabs-label, .nz-tabs-list, .nz-tabs-panel, .nz-tabs-sizer, .nz-tabs-tab, .nz-tabs-text.
Tokens this stylesheet reads (26)
--nz-bg-brand, --nz-bg-subtle, --nz-border-focus, --nz-border-strong, --nz-border-subtle, --nz-border-width-default, --nz-border-width-focus, --nz-duration-fast, --nz-easing-standard, --nz-font-label-m, --nz-font-label-m-strong-font-weight, --nz-font-label-s, --nz-radius-full, --nz-size-control-md, --nz-size-icon-sm, --nz-space-0-5, --nz-space-1, --nz-space-1-5, --nz-space-3, --nz-space-4, --nz-text-disabled, --nz-text-primary, --nz-text-secondary, --nz-tracking-label-m, --nz-tracking-label-m-strong, --nz-tracking-label-s
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:
tabs-tab: draws the focus outline itself on keyboard focus.
In forced-colours mode these states keep a visible system colour: tabs-tab[data-selected]::before (fill).
Behaviour comes from the react-aria engine through Nizivo's adapter: focus management, ARIA wiring and keyboard handling are the engine's.
Pattern: WAI-ARIA Authoring Practices.
Keyboard
| Keys | Action |
|---|---|
| Tab | Moves focus into the tab list, to the selected tab; the next Tab moves into the panel, to its first focusable element or the panel itself. |
| ArrowRight, ArrowLeft | Moves focus to the next or previous tab, wrapping at the ends and skipping disabled tabs, and selects it (in manual activation, only moves focus). The keys follow the strip's reading direction, which is the dir prop on its Tabs.Root or on the nearest Tabs.Root around it, otherwise the direction of the locale set on NizivoConfig. A dir attribute on any other element higher up the page does not flip them. |
| Home, End | Moves focus to the first or last tab, and selects it, except in manual activation. |
| Enter, Space | In manual activation, selects the focused tab. |
Source: react-aria-components 1.22.1 Tabs, TabList, Tab and TabPanel (react-aria 3.53.1 useTabList, useTab and useTabPanel) through _engines/react-aria/tabs.client.tsx. The browser lane (test/browser/tabs.spec.tsx) checks Tab entry, arrows, Home, End, wrapping, skipping a disabled tab, manual activation and the RTL flip, and that a dir on Tabs.Root overrides the NizivoConfig scope, that the nearest of nested scopes wins, that a strip nested in a dir root's panel follows that dir, that changing dir turns the arrows without remounting the tabs, and that a server render with dir hydrates without warnings.
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:14781 - Ghost masters: Figma node
755:961
TableSelection
Row checkboxes for Table's bulk actions. Native checkboxes rendered on the server submit the checked ids with their form before any JavaScript runs; one small client island adds select-all, Shift-click ranges and Escape.
Text
Body, label, numeric and mono text in the Nizivo text styles and text colours, as a paragraph or any element through render.