SegmentedControl
A compact single choice between a few options, such as a chart's time range. Native radios inside labels, so it submits, resets and works before hydration. Inside a Field, give the Field group.
Import
import { SegmentedControl } from '@nizivo/react/segmented-control';
// Once per app (Getting started), after
// @nizivo/tokens/tokens.css and @nizivo/react/css/base.css
import '@nizivo/react/css/segmented-control.css';Static or interactive
Client. It always ships its client leaf, which wires it to a Field. The server HTML is a native radio group, so arrows and submission work before hydration. When each value is its own URL, a TabNav of links does the job with no client code. How to choose.
Examples
Basic
The Overview range picker: a radio group, so arrow keys move the selection.
import type { JSX } from 'react';
import { SegmentedControl } from '@nizivo/react/segmented-control';
const RANGES = [
['today', 'Today'],
['7d', '7D'],
['30d', '30D'],
['90d', '90D'],
['12m', '12M'],
] as const;
export function SegmentedControlExample(): JSX.Element {
return (
<SegmentedControl.Root aria-label="Range" name="range" defaultValue="30d">
{RANGES.map(([value, label]) => (
<SegmentedControl.Item key={value} value={value}>
{label}
</SegmentedControl.Item>
))}
</SegmentedControl.Root>
);
}
Sizes
32, 40 and 48 px tracks, and a track that fills its container with equal items.
import type { JSX } from 'react';
import { Stack } from '@nizivo/react/stack';
import { SegmentedControl } from '@nizivo/react/segmented-control';
const RANGES = [
['today', 'Today'],
['7d', '7D'],
['30d', '30D'],
['90d', '90D'],
['12m', '12M'],
] as const;
export function SegmentedControlSizesExample(): JSX.Element {
return (
<Stack gap="4">
<SegmentedControl.Root aria-label="Range, small" size="sm" defaultValue="7d">
{RANGES.slice(0, 3).map(([value, label]) => (
<SegmentedControl.Item key={value} value={value}>
{label}
</SegmentedControl.Item>
))}
</SegmentedControl.Root>
<SegmentedControl.Root aria-label="Range, large" size="lg" defaultValue="7d">
{RANGES.slice(0, 3).map(([value, label]) => (
<SegmentedControl.Item key={value} value={value}>
{label}
</SegmentedControl.Item>
))}
</SegmentedControl.Root>
<div style={{ inlineSize: 'min(100%, 420px)' }}>
<SegmentedControl.Root aria-label="Range, full width" fullWidth defaultValue="30d">
{RANGES.map(([value, label]) => (
<SegmentedControl.Item key={value} value={value}>
{label}
</SegmentedControl.Item>
))}
</SegmentedControl.Root>
</div>
</Stack>
);
}
States
Icon-only items are named with aria-label; one item, or the whole group, can be disabled.
import type { JSX } from 'react';
import { Stack } from '@nizivo/react/stack';
import { SegmentedControl } from '@nizivo/react/segmented-control';
/** Stand-ins for `@nizivo/icons` glyphs (16 px Lucide strokes). */
function Rows(): JSX.Element {
return (
<svg
viewBox="0 0 24 24"
width="1em"
height="1em"
fill="none"
stroke="currentColor"
strokeWidth={1.92}
strokeLinecap="round"
>
<path d="M3 6h18M3 12h18M3 18h18" />
</svg>
);
}
function Tiles(): JSX.Element {
return (
<svg
viewBox="0 0 24 24"
width="1em"
height="1em"
fill="none"
stroke="currentColor"
strokeWidth={1.92}
strokeLinejoin="round"
>
<rect x="3" y="3" width="7" height="7" rx="1" />
<rect x="14" y="3" width="7" height="7" rx="1" />
<rect x="3" y="14" width="7" height="7" rx="1" />
<rect x="14" y="14" width="7" height="7" rx="1" />
</svg>
);
}
export function SegmentedControlStatesExample(): JSX.Element {
return (
<Stack gap="4">
<SegmentedControl.Root aria-label="Layout" defaultValue="list">
<SegmentedControl.Item value="list" icon={<Rows />} aria-label="List" />
<SegmentedControl.Item value="grid" icon={<Tiles />} aria-label="Grid" />
</SegmentedControl.Root>
<SegmentedControl.Root aria-label="Range with a disabled item" defaultValue="7d">
<SegmentedControl.Item value="7d">7D</SegmentedControl.Item>
<SegmentedControl.Item value="30d">30D</SegmentedControl.Item>
<SegmentedControl.Item value="12m" disabled>
12M
</SegmentedControl.Item>
</SegmentedControl.Root>
<SegmentedControl.Root aria-label="Range, disabled" defaultValue="30d" disabled>
<SegmentedControl.Item value="7d">7D</SegmentedControl.Item>
<SegmentedControl.Item value="30d">30D</SegmentedControl.Item>
</SegmentedControl.Root>
</Stack>
);
}
Field
Inside a Field, the Field's label names the group and its description and error describe it. required with no default value blocks submit until a choice is made.
import type { JSX } from 'react';
import { Field } from '@nizivo/react/field';
import { Stack } from '@nizivo/react/stack';
import { SegmentedControl } from '@nizivo/react/segmented-control';
export function SegmentedControlFieldExample(): JSX.Element {
return (
<Stack gap="6">
<Field
label="Payout schedule"
description="When we send your earnings."
name="schedule"
group
>
<SegmentedControl.Root defaultValue="weekly">
<SegmentedControl.Item value="daily">Daily</SegmentedControl.Item>
<SegmentedControl.Item value="weekly">Weekly</SegmentedControl.Item>
<SegmentedControl.Item value="monthly">Monthly</SegmentedControl.Item>
</SegmentedControl.Root>
</Field>
<Field label="Delivery speed" name="speed" required error="Choose a delivery speed." group>
<SegmentedControl.Root>
<SegmentedControl.Item value="standard">Standard</SegmentedControl.Item>
<SegmentedControl.Item value="express">Express</SegmentedControl.Item>
</SegmentedControl.Root>
</Field>
</Stack>
);
}
Parts
Use the parts through the namespace (SegmentedControl.Root) or by their flat names (SegmentedControlRoot); both work from Server Components.
SegmentedControl.Root
Client component Renders data-slot="segmented-control" with the class nz-segmented-control. Props: SegmentedControlRootProps. Source: src/segmented-control/segmented-control.client.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
children | ReactNode | – | |
defaultValue | string | – | The item selected at first (uncontrolled; |
disabled | boolean | – | Disables every item. |
form | string | – | The |
fullWidth | boolean | false | Stretches the track to its container and shares the width equally between items. |
invalid | boolean | – | An error message is presented (set by a Field). |
name | string | – | The radios' shared name, and the |
onValueChange | (value: string, details: ChangeDetails<SegmentedControlChangeReason>) => void | – | Called when the user selects an item. |
render | RenderProp<SegmentedControlRootState> | – | |
required | boolean | – | |
size | ControlSize"sm""md""lg" | 'md' | 32 / 40 / 48 px track. |
value | string | null | – | Controlled: the selected item's |
- Also takes every attribute of
<div>exceptdefaultValue,onChange,role;childrenis redeclared above. (Omit<ComponentPropsWithRef<"div">, "defaultValue" | "onChange" | "role">)
render takes an element, or, in client code, a function of the props and this state (SegmentedControlRootState):
| Name | Type | Default | Description |
|---|---|---|---|
disabledrequired | boolean | – | |
invalidrequired | boolean | – | |
sizerequired | ControlSize"sm""md""lg" | – |
SegmentedControl.Item
Client component Renders data-slot="segmented-control-item" with the class nz-segmented-control-item. Props: SegmentedControlItemProps. Source: src/segmented-control/segmented-control.client.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
aria-label | string | – | Names an icon-only item (no |
children | ReactNode | – | |
disabled | boolean | – | |
icon | ReactNode | – | A decorative glyph before the text (16 px). |
render | RenderProp<SegmentedControlItemState> | – | |
valuerequired | string | – | This item's value: what |
- Also takes every attribute of
<label>exceptaria-label,htmlFor;childrenis redeclared above. (Omit<ComponentPropsWithRef<"label">, "htmlFor" | "aria-label">)
render takes an element, or, in client code, a function of the props and this state (SegmentedControlItemState):
| Name | Type | Default | Description |
|---|---|---|---|
disabledrequired | boolean | – |
Other exports
SegmentedControlChangeReasontypeimport type { SegmentedControlChangeReason } from '@nizivo/react/segmented-control'Why the value changed: the user checked a radio (a pointer or a key; the browser does not say).
type SegmentedControlChangeReason = "input"SegmentedControlItemPropstypeimport type { SegmentedControlItemProps } from '@nizivo/react/segmented-control'The props of
SegmentedControl.Item, listed above.SegmentedControlItemStatetypeimport type { SegmentedControlItemState } from '@nizivo/react/segmented-control'The render state of
SegmentedControl.Item, listed above.SegmentedControlRootPropstypeimport type { SegmentedControlRootProps } from '@nizivo/react/segmented-control'The props of
SegmentedControl.Root, listed above.SegmentedControlRootStatetypeimport type { SegmentedControlRootState } from '@nizivo/react/segmented-control'The render state of
SegmentedControl.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 |
|---|---|---|
segmented-control.nz-segmented-control | SegmentedControl.Rootroot (takes render) |
|
segmented-control-icon.nz-segmented-control-icon | SegmentedControl.Iteminner element (CSS only) | none |
segmented-control-input.nz-segmented-control-input | SegmentedControl.Iteminner element (CSS only) | none |
segmented-control-item.nz-segmented-control-item | SegmentedControl.Itemroot (takes render); interactive, focus: nested-input; forced colours: outline on :has(> input:checked) |
|
segmented-control-label.nz-segmented-control-label | SegmentedControl.Iteminner 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-segmented-control-bg | --nz-bg-subtle |
|
--nz-segmented-control-fg | --nz-text-secondary |
|
--nz-segmented-control-fg-selected | --nz-text-primary |
|
--nz-segmented-control-gap | --nz-space-0-5 |
|
--nz-segmented-control-height |
|
|
--nz-segmented-control-radius |
|
|
--nz-segmented-control-thumb-bg | --nz-bg-surface |
|
Layers: nz.components. Classes: .nz-segmented-control, .nz-segmented-control-icon, .nz-segmented-control-input, .nz-segmented-control-item, .nz-segmented-control-label, .nz-segmented-control-sizer, .nz-segmented-control-text.
Tokens this stylesheet reads (27)
--nz-bg-subtle, --nz-bg-surface, --nz-border-control, --nz-border-critical, --nz-border-subtle, --nz-border-width-default, --nz-duration-fast, --nz-easing-standard, --nz-elevation-1, --nz-font-label-l, --nz-font-label-m, --nz-font-label-m-strong-font-weight, --nz-radius-md, --nz-radius-sm, --nz-size-control-lg, --nz-size-control-md, --nz-size-control-sm, --nz-size-icon-sm, --nz-space-0-5, --nz-space-1, --nz-space-1-5, --nz-space-2, --nz-space-3, --nz-space-4, --nz-text-disabled, --nz-text-primary, --nz-text-secondary
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:
segmented-control-item: draws the outline when the hidden input inside it has keyboard focus.
In forced-colours mode these states keep a visible system colour: segmented-control-item:has(> input:checked) (outline).
Pattern: WAI-ARIA Authoring Practices.
Keyboard
| Keys | Action |
|---|---|
| Tab | Moves focus into the control, to the selected item; the next Tab leaves it. |
| ArrowRight, ArrowDown | Selects the next item and focuses it, skipping disabled ones. |
| ArrowLeft, ArrowUp | Selects the previous item and focuses it, skipping disabled ones. |
Source: native <input type="radio"> elements inside <label>s (segmented-control.client.tsx); its only key handler marks the item data-focus-visible, because WebKit matches no :focus-visible on a radio reached by an arrow key. The browser lane (test/browser/segmented-control.spec.tsx) checks Tab entry, arrows and skipping a disabled item.
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:14797 - Ghost masters: Figma node
755:982
SearchField
A search or filter input in a <search> landmark, with a magnifier, a clear button and an optional shortcut hint. Inside a <form method="get"> it filters with no JavaScript; inside a Field it is labelled, described and validated by the Field. It is also the top bar's search, as a real input.
Select
A single-choice list box with a button trigger, groups and separators, backed by a hidden native select so forms submit and reset as usual.