SegmentedControl
A compact row of two to five mutually exclusive options that applies immediately — a mode or view switch, not a form field. A RadioGroup in a toolbar's clothing.
Basic usage
Install the package:
pnpm add @design-schema/react @design-schema/tokens Import the stylesheet once for the app, then the component:
import '@design-schema/tokens/calm-precise/css';
import '@design-schema/react/index.css';
import { SegmentedControl } from '@design-schema/react'; Examples
import { SegmentedControl, type SegmentedControlOption } from '@design-schema/react';
const OPTIONS: SegmentedControlOption[] = [
{ value: 'day', label: 'Day' },
{ value: 'week', label: 'Week' },
{ value: 'month', label: 'Month' },
];
<SegmentedControl label="Range" options={OPTIONS} />Props
7 rows| Prop | Type | Default | Description |
|---|---|---|---|
| label required | string | — | Accessible name of the control ("View mode"). Not shown; put a visible Text label beside it when the meaning is not obvious from context. Lit, where an attribute can be absent, defaults the property to an empty string and warns in development when it is empty; React and React Native rely on the required type and do not warn, even for an empty string. |
| options required | array { value: string; label: string; icon?: IconName | undefined; disabled?: boolean | undefined }[] | — | Two to five options (guidance, not enforced: any count renders, with no warning). An empty array is a documented no-op: an empty group renders with no tab stop and no pill, keys are ignored, and nothing warns. Labels are one word; with `iconOnly` the label becomes the accessible name. The icon is an Icon whose `size` is the control's `size` (`sm` or `md`), on every platform, and whose colour is its segment's own — `segmentColor`, or `segmentSelectedColor` when selected — forwarded as the Icon's `overrides.color`, since there is no `currentColor` on native. |
| value | string | — | Controlled selected value. Omit for uncontrolled. |
| defaultValue | string | — | Initially selected value. Defaults to the first enabled option — a segmented control always has a selection. A `value` or `defaultValue` is taken as given, never corrected: one naming a disabled option keeps that segment checked with the pill under it (arrows still skip it); one matching no option checks nothing and draws no pill (the pill is unmounted, and the next selection places it instantly rather than sliding it in). In both cases the tab stop is the first enabled segment, and arrows move from there when no segment has focus. |
| iconOnly | boolean | false | Show icons only (every option must have one); labels become accessible names and Tooltips, disabled segments included — an `aria-disabled` button still takes pointer events, so its label stays discoverable on hover. An option without `icon` warns in development once per instance (one message listing every option without an icon by its `value`, since that is the stable identifier; the wording itself is a development aid, not copy, so it is not contract) and that segment shows its label as text instead, at its natural width — no icon-sized box is reserved for it — so it never renders empty; that segment gets no Tooltip and no `aria-label`, since its visible text is its name. |
| size | sm | md | md | Toolbar (`sm`) or standard (`md`) height. |
| fill | boolean | false | Stretch to the container width with equal segments. Off, the group hugs its segments rather than filling the container (React Native: `alignSelf: 'flex-start'`, which is what keeps `fill` from being a no-op there). |
Events
1 row| Event | Type | Description |
|---|---|---|
| onChange | onChange / change | Fired when the selection changes, with the new value. The change takes effect immediately. |
Accessibility
Decisions this component leaves to you
Supply an accessible name. This component has no text of its own to fall back on, so without one a screen reader announces its role and nothing else.
Implicit ARIA role: radiogroup.
Anatomy
What generation guarantees
Accessible name — Every instance carries a programmatic name. Where the component has no text of its own to use, the name is a prop the consumer supplies.
Selected state — Selection is exposed programmatically (`aria-selected` / `aria-checked`).
Arrow navigation — Arrow keys move between items; Home and End jump to the ends.
Roving tabindex — The group is a single tab stop; the arrow keys move within it.
Keyboard operable — Everything the pointer can do here, the keyboard can do — see the keyboard section.
Visible focus — Keyboard focus is always drawn, from the theme’s focus tokens, and is never removed.
Contrast — Text and meaningful non-text contrast meets WCAG AA; the pairs are listed below.
Target size — Interactive targets are at least 24×24 CSS pixels (WCAG 2.2 Target Size, minimum).
Reduced motion — Under `prefers-reduced-motion: reduce` the transitions are dropped rather than shortened, and the component still reaches every state.
Under `prefers-reduced-motion: reduce` the transitions are dropped rather than shortened, and the component still reaches every state.