SegmentedControl
A row of mutually exclusive options with exactly one chosen at a time, for switching a single view between a small set of windows.
Preview
segmented-control · base base · style base-lyraOpen under the product themeInstallation
pnpm dlx shadcn@latest add @opsinjs/segmented-control
The @opsinjs namespace is declared in your components.json. Everything it installs is code you then own. There is no runtime package to keep in step.
Usage
import { SegmentedControl } from "@/components/ui/segmented-control"<SegmentedControl
label="Chart window"
value={chartWindow}
onValueChange={setChartWindow}
options={[
{ value: "day", label: "Day" },
{ value: "week", label: "Week" },
{ value: "month", label: "Month" },
]}
/>When to use it
Use it when
- Switching one view between a small set of mutually exclusive windows, such as a chart's day, week or month range.
- Choosing one setting from two to five short, comparable options that fit on one row and read as siblings.
- A choice the reader changes often and wants the whole set in front of them.
Do not use it when
- The options open different views with their own content, so choosing one swaps what the reader is looking at.Use
tabsinstead. - There are more than about five options, or the labels are long, so the row will not fit one line.Use
fieldinstead. - The control performs an action rather than setting a value, such as saving a reading.Use
buttoninstead.
Anatomy
SegmentedControlThe root. A radiogroup on Base UI's Composite primitive, so the whole control is one tab stop.Controlled byoptionsSegmentedControl.SegmentOne option, a Base UI Radio with role="radio" and aria-checked. A disabled segment drops to muted ink.Controlled byvalueSegmentedControl.LabelThe option's words, the visible label and part of what a screen reader reads for the option.
- SegmentedControl 1role="radiogroup", one roving tab stop
- SegmentedControl.Segment 2..nrole="radio", aria-checked; one per option
- SegmentedControl.Label 1
- SegmentedControl.Segment 2..nrole="radio", aria-checked; one per option
Examples
Switching a chart window
The three options set the window of one chart rather than swapping between three charts, which is why it is a radiogroup. The chart is a placeholder.
segmented-control-switching-a-chart-window · base base · style base-lyraOpen under the product themeContent guidelines
Keep the labels short, parallel and in sentence case, one or two words each, so the row reads as siblings. Name the value the option sets, not an instruction, and give it no terminal punctuation. Order them the way the reader thinks, shortest to longest, and keep one wording across screens.
Do
"Day" · "Week" · "Month" Three parallel windows, one word each, ordered shortest to longest.
Don’t
"Today" · "This week's readings" · "Show me the month" Three lengths and three grammars, so the row stops reading as one set.
Accessibility
Audited against WCAG 2.2 AA, in a source pass over the full rubric and a rendered pass in the browser. The audit is author-run, not an independent review, and clinical review is still pending.
A radio group, so the row is one tab stop with a roving focus, with
radiogroup plus radio and aria-checked per segment for name, role and
value. Selection is carried three ways and never by colour alone: aria-checked,
the card surface lifting the segment off the track, and a hairline.
What the audit changed here. The 44px hit-area floor sat on the height axis
only, so a single-character label or the sm size could shrink a segment below
the house minimum. The floor now sits on both axes, matching scale-input and
tab-bar.
What a reader should still know. label is applied only as the group's
aria-label, so there is no persistent visible group heading. Every option
carries visible text, so the choices are seen, but a visible group label is a
recommended enhancement, deferred as one repair across the wrappers that share
this pattern. No contrast pair has been measured, so greyscale legibility of the
selected segment is argued, not confirmed.
| Key | Action | Notes |
|---|---|---|
| Tab | Moves focus into the control, onto the selected segment | The group is one tab stop. A disabled segment never takes it. |
| Shift+Tab | Moves focus back out of the control | One stop in reverse too, so focus leaves the whole row. |
| Arrow Right, Arrow Down | Moves to the next segment and selects it | Selection follows focus. It wraps, skips disabled segments and mirrors under right-to-left. |
| Arrow Left, Arrow Up | Moves to the previous segment and selects it | Wraps from the first segment to the last and skips disabled segments. |
| Space | Selects the focused segment | A segment is a role="radio", so Space is its activator. |
| Enter | Does nothing | The primitive cancels Enter, so a form is never submitted by accident. |
| Home, End | Not bound | The RadioGroup primitive disables them, so the browser's default applies. |
| Pair | Theme | APCA Lc | WCAG 2.2 | Floor |
|---|---|---|---|---|
| body text on the page | light | 101.6 | 17.18:1 | Pass |
| body text on the page | dark | -100.5 | 18.00:1 | Pass |
| body text on a card | light | 104.7 | 17.96:1 | Pass |
| body text on a card | dark | -99.6 | 16.32:1 | Pass |
| body text on the muted ground | light | 98.1 | 16.32:1 | Pass |
| body text on the muted ground | dark | -97.5 | 13.77:1 | Pass |
| secondary text on the page | light | 83.9 | 7.42:1 | Pass |
| secondary text on the page | dark | -80.7 | 13.44:1 | Pass |
| secondary text on a card | light | 87.0 | 7.76:1 | Pass |
| secondary text on a card | dark | -79.8 | 12.19:1 | Pass |
| secondary text on the muted ground | light | 80.4 | 7.05:1 | Pass |
| secondary text on the muted ground | dark | -77.8 | 10.28:1 | Pass |
| a hairline boundary on the page | light | 19.3 | 1.41:1 | Below floor |
| a hairline boundary on the page | dark | -8.2 | 1.90:1 | Below floor |
| a hairline boundary on a card | light | 22.4 | 1.47:1 | Below floor |
| a hairline boundary on a card | dark | -7.3 | 1.72:1 | Below floor |
| a hairline boundary on the muted ground | light | 15.8 | 1.34:1 | Below floor |
| a hairline boundary on the muted ground | dark | 0.0 | 1.45:1 | Below floor |
| a placeholder boundary on the page | light | 69.8 | 4.52:1 | Pass |
| a placeholder boundary on the page | dark | -51.0 | 7.62:1 | Pass |
| a placeholder boundary on a card | light | 72.8 | 4.72:1 | Pass |
| a placeholder boundary on a card | dark | -50.1 | 6.91:1 | Pass |
| a placeholder boundary on the muted ground | light | 66.2 | 4.29:1 | Pass |
| a placeholder boundary on the muted ground | dark | -48.1 | 5.83:1 | Pass |
| a placeholder fill on the page | light | 8.3 | 1.18:1 | Below floor |
| a placeholder fill on the page | dark | -8.2 | 1.90:1 | Below floor |
| a placeholder fill on a card | light | 11.4 | 1.23:1 | Below floor |
| a placeholder fill on a card | dark | -7.3 | 1.72:1 | Below floor |
| a placeholder fill on the muted ground | light | 0.0 | 1.12:1 | Below floor |
| a placeholder fill on the muted ground | dark | 0.0 | 1.45:1 | Below floor |
| a placeholder boundary on its own fill | light | 58.9 | 3.84:1 | Pass |
| a placeholder boundary on its own fill | dark | -41.5 | 4.01:1 | Below floor |
| the card hairline on the page | light | 69.8 | 4.52:1 | Pass |
| the card hairline on the page | dark | -28.7 | 4.19:1 | Below floor |
| the card hairline on a card | light | 72.8 | 4.72:1 | Pass |
| the card hairline on a card | dark | -27.9 | 3.80:1 | Below floor |
These are the measured token pairs this component draws colour from, not a measurement of the component itself.
API reference
Prop
Type
Generated from SegmentedControlProps in registry/bases/base/segmented-control.tsx.
value and onValueChange make this controlled, with no internal selection
state. A value matching no option renders the row with nothing chosen and
raises a development warning. label is required: a radiogroup with no
accessible name is a defect the type system cannot otherwise prevent.
Related
Menu
A list of actions opened from a button, built on Base UI Menu, that draws neutral chrome and keeps every safety-relevant action out on the screen.
TabBar
The persistent bar of top-level destinations at the foot of a phone screen, marking one as current with a heavier label and a top indicator rather than colour.