Skip to content
Nizivo UI

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.

Tier: client

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.

When we send your earnings.
Choose a delivery speed.
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.

NameTypeDefaultDescription
childrenReactNode–
defaultValuestring–

The item selected at first (uncontrolled; form.reset() returns to it).

disabledboolean–

Disables every item.

formstring–

The <form> the radios belong to, when they are outside it.

fullWidthbooleanfalse

Stretches the track to its container and shares the width equally between items.

invalidboolean–

An error message is presented (set by a Field).

namestring–

The radios' shared name, and the FormData key. Without one (and outside a named Field), a generated name groups the radios.

onValueChange(value: string, details: ChangeDetails<SegmentedControlChangeReason>) => void–

Called when the user selects an item. details.reason is 'input' (as on RadioGroup and Checkbox); details.event is the native change event.

renderRenderProp<SegmentedControlRootState>–
requiredboolean–
sizeControlSize
"sm""md""lg"
'md'

32 / 40 / 48 px track.

valuestring | null–

Controlled: the selected item's value, or null for none.

  • Also takes every attribute of <div> except defaultValue, onChange, role; children is 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):

NameTypeDefaultDescription
disabledrequiredboolean–
invalidrequiredboolean–
sizerequiredControlSize
"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.

NameTypeDefaultDescription
aria-labelstring–

Names an icon-only item (no children): it goes on the radio, which is what assistive technology announces.

childrenReactNode–
disabledboolean–
iconReactNode–

A decorative glyph before the text (16 px).

renderRenderProp<SegmentedControlItemState>–
valuerequiredstring–

This item's value: what onValueChange receives and the form submits.

  • Also takes every attribute of <label> except aria-label, htmlFor; children is 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):

NameTypeDefaultDescription
disabledrequiredboolean–

Other exports

SegmentedControlChangeReason type

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

SegmentedControlItemProps type

import type { SegmentedControlItemProps } from '@nizivo/react/segmented-control'

The props of SegmentedControl.Item, listed above.

SegmentedControlItemState type

import type { SegmentedControlItemState } from '@nizivo/react/segmented-control'

The render state of SegmentedControl.Item, listed above.

SegmentedControlRootProps type

import type { SegmentedControlRootProps } from '@nizivo/react/segmented-control'

The props of SegmentedControl.Root, listed above.

SegmentedControlRootState type

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

SlotRendered byData attributes
segmented-control
.nz-segmented-control
SegmentedControl.Root
root (takes render)
  • data-disabled (present or absent) ← disabled
    Mirrors the disabled attribute
  • data-full-width (present or absent) ← fullWidth
    The fullWidth prop: the control fills the inline size of its container
  • data-invalid (present or absent) ← invalid
    An error message is currently presented (not "fails constraints"); always the Field's own state, never the engine's
  • data-required (present or absent) ← required
    Mirrors the required attribute
  • data-size: "sm""md""lg" ← size
    The size prop; values in each component manifest
segmented-control-icon
.nz-segmented-control-icon
SegmentedControl.Item
inner element (CSS only)
none
segmented-control-input
.nz-segmented-control-input
SegmentedControl.Item
inner element (CSS only)
none
segmented-control-item
.nz-segmented-control-item
SegmentedControl.Item
root (takes render); interactive, focus: nested-input; forced colours: outline on :has(> input:checked)
  • data-disabled (present or absent) ← disabled
    Mirrors the disabled attribute
segmented-control-label
.nz-segmented-control-label
SegmentedControl.Item
inner 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.

HookDefaultRead as
--nz-segmented-control-bg--nz-bg-subtle
  • background on .nz-segmented-control
--nz-segmented-control-fg--nz-text-secondary
  • color on .nz-segmented-control-item
--nz-segmented-control-fg-selected--nz-text-primary
  • color on .nz-segmented-control-item:has( > input:checked)
--nz-segmented-control-gap--nz-space-0-5
  • gap on .nz-segmented-control
--nz-segmented-control-height
  • default: var(--nz-size-control-md)
  • data-size="lg": var(--nz-size-control-lg)
  • data-size="sm": var(--nz-size-control-sm)
  • block-size on .nz-segmented-control
--nz-segmented-control-radius
  • default: var(--nz-radius-md)
  • data-size="sm": var(--nz-radius-sm)
  • border-radius on .nz-segmented-control
  • border-radius on .nz-segmented-control-item
--nz-segmented-control-thumb-bg--nz-bg-surface
  • background on .nz-segmented-control-item:has( > input:checked)

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

KeysAction
TabMoves focus into the control, to the selected item; the next Tab leaves it.
ArrowRight, ArrowDownSelects the next item and focuses it, skipping disabled ones.
ArrowLeft, ArrowUpSelects 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.

On this page