SegmentedControl
$ npx shadcn@latest add https://lacodda.github.io/dowel/r/segmented-control.jsonFor 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
| Prop | Type | Default | |
|---|---|---|---|
aria-label | string | What 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-labelledby | string | The id of the element that names the question, when a visible heading already does. Use it instead of aria-label. | |
value | string | The chosen segment's value, for a controlled control. Pair it with onValueChange. | |
defaultValue | string | The segment chosen first, for an uncontrolled control. | |
onValueChange | (value: string) => void | Called with the new segment's value each time the choice changes. | |
disabled | boolean | Disables every segment at once. | |
name | string | The field name, for a form that posts it. | |
className | string | ||
childrenrequired | ReactNode |
Segment
One option of a SegmentedControl.
| Prop | Type | Default | |
|---|---|---|---|
valuerequired | string | What the control's value becomes when this segment is chosen. | |
childrenrequired | ReactNode | The option's words. An icon alone needs aria-label beside it. | |
aria-label | string | The segment's name for a screen reader. Needed when the content is an icon alone. | |
disabled | boolean | Disables this segment only. | |
className | string |
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.