opsinjs
ComponentsNavigation

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

Theme
Density
Text
Status
segmented-control · base base · style base-lyraOpen under the product theme

Installation

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 tabs instead.
  • There are more than about five options, or the labels are long, so the row will not fit one line.Use field instead.
  • The control performs an action rather than setting a value, such as saving a reading.Use button instead.

Anatomy

  1. SegmentedControlThe root. A radiogroup on Base UI's Composite primitive, so the whole control is one tab stop.Controlled by options
  2. SegmentedControl.SegmentOne option, a Base UI Radio with role="radio" and aria-checked. A disabled segment drops to muted ink.Controlled by value
  3. SegmentedControl.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

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.

Theme
Density
Text
Status
segmented-control-switching-a-chart-window · base base · style base-lyraOpen under the product theme

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

KeyActionNotes
TabMoves focus into the control, onto the selected segmentThe group is one tab stop. A disabled segment never takes it.
Shift+TabMoves focus back out of the controlOne stop in reverse too, so focus leaves the whole row.
Arrow Right, Arrow DownMoves to the next segment and selects itSelection follows focus. It wraps, skips disabled segments and mirrors under right-to-left.
Arrow Left, Arrow UpMoves to the previous segment and selects itWraps from the first segment to the last and skips disabled segments.
SpaceSelects the focused segmentA segment is a role="radio", so Space is its activator.
EnterDoes nothingThe primitive cancels Enter, so a form is never submitted by accident.
Home, EndNot boundThe RadioGroup primitive disables them, so the browser's default applies.
PairThemeAPCA LcWCAG 2.2Floor
body text on the pagelight101.617.18:1Pass
body text on the pagedark-100.518.00:1Pass
body text on a cardlight104.717.96:1Pass
body text on a carddark-99.616.32:1Pass
body text on the muted groundlight98.116.32:1Pass
body text on the muted grounddark-97.513.77:1Pass
secondary text on the pagelight83.97.42:1Pass
secondary text on the pagedark-80.713.44:1Pass
secondary text on a cardlight87.07.76:1Pass
secondary text on a carddark-79.812.19:1Pass
secondary text on the muted groundlight80.47.05:1Pass
secondary text on the muted grounddark-77.810.28:1Pass
a hairline boundary on the pagelight19.31.41:1Below floor
a hairline boundary on the pagedark-8.21.90:1Below floor
a hairline boundary on a cardlight22.41.47:1Below floor
a hairline boundary on a carddark-7.31.72:1Below floor
a hairline boundary on the muted groundlight15.81.34:1Below floor
a hairline boundary on the muted grounddark0.01.45:1Below floor
a placeholder boundary on the pagelight69.84.52:1Pass
a placeholder boundary on the pagedark-51.07.62:1Pass
a placeholder boundary on a cardlight72.84.72:1Pass
a placeholder boundary on a carddark-50.16.91:1Pass
a placeholder boundary on the muted groundlight66.24.29:1Pass
a placeholder boundary on the muted grounddark-48.15.83:1Pass
a placeholder fill on the pagelight8.31.18:1Below floor
a placeholder fill on the pagedark-8.21.90:1Below floor
a placeholder fill on a cardlight11.41.23:1Below floor
a placeholder fill on a carddark-7.31.72:1Below floor
a placeholder fill on the muted groundlight0.01.12:1Below floor
a placeholder fill on the muted grounddark0.01.45:1Below floor
a placeholder boundary on its own filllight58.93.84:1Pass
a placeholder boundary on its own filldark-41.54.01:1Below floor
the card hairline on the pagelight69.84.52:1Pass
the card hairline on the pagedark-28.74.19:1Below floor
the card hairline on a cardlight72.84.72:1Pass
the card hairline on a carddark-27.93.80:1Below 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.

  • Tabs swap between distinct panels of content. This control sets a parameter of one view.
  • Field with a select is the control for a choice too large or too wordy for one row.
  • Button performs an action rather than setting a value.

On this page