Segmented control
Switches between a few views of one thing.
Sizes
sm
defaultdefault
lg
Multiple
falsedefault
true
States
rest
hover
held
focus-visible
chosen
disabled
Usage
When to use it
- Use
- Two to five views of the same content: all, errors, warnings; day, week, month.
- Use
- A filter where any number can be on at once (multiple).
- Not for
- A setting saved with a form, where each choice needs a sentence. Use Radio group.
- Not for
- A single on or off. Use Switch.
- Not for
- Pages with their own content and URL: use tabs.
Rules
7 rules
- Must
- Pass value or defaultValue to a single control. Because nothing is chosen until the first press, and a radio can never go back to none, so the view below has no state to show.
- Must
- Name the control with aria-label or aria-labelledby. Because a screen reader announces "radio group" with no question, then the bare segment names.
- Must
- Put a number in count, never in the label. Because count is tabular, reserves its width and is read as a description, so the name stays "Errors" while it ticks and the segments never move.
- Never
- Fill the track or a segment with a hue, or restyle the chosen thumb. Because the small glaze thumb is the chosen segment, the press recipe the ticked box and the switch share, and a hue reads as a status.
- Should
- Keep it to two to five segments with labels of one or two words. Because a sixth segment or a long label squeezes every segment, and a label that ends in an ellipsis hides the view's name.
- Should
- Pass countMax when a count changes live, so its slot is reserved from the start.
- Should
- Match size to the buttons in the same row: sm in a toolbar of sm buttons.
State order
6 states, highest first
- 1
- disabled
- 2
- chosen
- 3
- held
- 4
- focus-visible
- 5
- hover
- 6
- rest
Accessibility
5 checks
- Single is a radiogroup with one tab stop; each segment is a radio. Multiple is a group of toggle buttons with aria-pressed.
- The count is aria-hidden and reaches the segment as its description.
- The chosen segment reads from the glaze thumb under it and its white label, 6.7:1 or more, against text-2 ones, not from colour.
- A label cut by an ellipsis keeps its full text as the title and the name.
- Focus is the 2px ink ring around the segment.
Keyboard
3 keys
- Arrow keys
- Single: move to the next segment and choose it. Multiple: move focus only.
- Space
- Chooses the focused segment, or turns it on or off with multiple.
- Tab
- Enters on the chosen segment and leaves the control.
Edge cases
7 edge cases
- Count goes from 99 to 100
- The slot was reserved for the widest value seen, or countMax, so nothing moves.
- Narrow container
- Labels end in an ellipsis; with fill the segments share the width equally.
- Disabled segment
- Arrow keys skip it; its label turns the disabled colour.
- Right to left
- Segments run right to left and the arrows follow.
- Changing the choice
- Single: one thumb slides to the new segment and takes its width in --dur-slide while the labels crossfade, and reduced motion jumps. Multiple: each pressed segment is its own thumb, no slide.
- Dark theme
- The thumb turns light glaze with a dark label on the dark paper track.
- Forced colors
- The track draws a CanvasText border; the chosen segment takes Highlight.
API
6 props
multiplebooleandefaultfalsevalue, defaultValue, onValueChangestring | string[]size'sm' | 'default' | 'lg'default'default'fillbooleandefaultfalsecountMaxnumberSegmentedControlItem value, count, countMax, disabledstring, number, number, boolean
Tokens
22 tokens
--paper- #ffffff
--line-faint- rgb(5 5 5 / 0.06)
--hover- rgb(5 5 5 / 0.06)
--text-1- rgb(5 5 5 / 0.80)
--text-2- rgb(5 5 5 / 0.60)
--text-disabled- rgb(5 5 5 / 0.32)
--glaze-sm-top- #6b6b6b
--glaze-sm-mid- #3d3d3d
--glaze-sm-bottom- #232323
--glaze-sm-highlight- rgb(255 255 255 / 0.30)
--glaze-sm-rim- rgb(0 0 0 / 0.55)
--glaze-sm-text- rgb(255 255 255 / 0.92)
--control-sm- 26px
--control- 32px
--control-lg- 36px
--radius-pill- 999px
--type-label- 500 13px/1.35 var(--font-sans)
--type-label-sm- 500 12px/1.35 var(--font-sans)
--type-label-lg- 500 14px/1.35 var(--font-sans)
--focus- rgb(5 5 5 / 0.80)
--dur-slide- 200ms
--ease-out- cubic-bezier(0.2, 0.8, 0.2, 1)