Skip to content

SegmentedControl

Terminal window
$ npx shadcn@latest add https://lacodda.github.io/dowel/r/segmented-control.json
See it liveA theme picker, options with icons, a disabled control, and one at compact density beside a button.→

For a choice you can see whole. The theme, a list shown as rows or cards, “all / mine / shared”. The shape settles three things a Select cannot: every option is on screen, the choice is one press rather than two, and switching back and forth is the same press again. kilna put a Select where a segment belongs — the theme in its settings — and drew the segment by hand where it had one.

<SegmentedControl aria-label={t('theme')} value={theme} onValueChange={setTheme}>
<Segment value="light">{t('light')}</Segment>
<Segment value="dark">{t('dark')}</Segment>
<Segment value="system">{t('system')}</Segment>
</SegmentedControl>

It is a radio group, not a row of toggles. Exactly one option is ever chosen, which is what a radio says and a toggle does not: a toggle group lets the last pressed button go, and “no theme” is not a theme. Base UI’s RadioGroup underneath gives the rest — one stop on the Tab key, the arrows move and choose at once, and a reader hears “Dark, radio button, 2 of 3”. For filters that can all be off, use a ChipGroup.

The chosen segment is lifted, not filled. A sunken track, and the chosen option raised out of it on the raised surface in semibold — the mockup’s .seg. An accent fill reads as “press me”, which the chosen option is the one thing you need not. The weight carries the choice too, so it survives a screen where the two greys look alike.

It stands on the small control row. Its height is h-control-sm, so it sits level with the small buttons and fields beside it and follows data-density with them.

An icon alone needs a name. A segment’s words are its accessible name; a segment that is only an icon takes aria-label.

Props

Read from the types in ui/segmented-control.tsx, the file a product copies - so the description of each prop is also what an editor shows over it there.

SegmentedControl

PropTypeDefault
aria-labelstringWhat is being chosen: "Theme", "View". Required - a row of words with no name for the question is a riddle to anyone who cannot see the heading above it.
aria-labelledbystringThe id of the element that names the question, when a visible heading already does. Use it instead of aria-label.
valuestringThe chosen segment's value, for a controlled control. Pair it with onValueChange.
defaultValuestringThe segment chosen first, for an uncontrolled control.
onValueChange(value: string) => voidCalled with the new segment's value each time the choice changes.
disabledbooleanDisables every segment at once.
namestringThe field name, for a form that posts it.
classNamestring
childrenrequiredReactNode

Segment

One option of a SegmentedControl.

PropTypeDefault
valuerequiredstringWhat the control's value becomes when this segment is chosen.
childrenrequiredReactNodeThe option's words. An icon alone needs aria-label beside it.
aria-labelstringThe segment's name for a screen reader. Needed when the content is an icon alone.
disabledbooleanDisables this segment only.
classNamestring

Accessibility

The examples on the stand, through axe 4.14.0 in a browser, in both themes, against WCAG 2.2 A and AA and axe's best practices: 18 rules applied and passed, and none failed. The accessibility report has every item.

History

Each release that changed ui/segmented-control.tsx - the file a product copies - newest first.

  1. v0.41.0
    • docs read every props table from the component's types ee3b159
  2. v0.33.0
    • feat one of a few, as a radio group 93eec94