Dev view
Segmented Control
A row (or column) of connected buttons that lets the user switch between mutually-exclusive views of the same content — list vs grid, day vs week vs month, KB vs MB vs GB unit toggles. Distinct from Tabs (which switches between different *content*), distinct from RadioGroup (which captures form values rather than view state), distinct from Toggle (which is binary, not n-way). Used for content-presentation toggles where the underlying data is the same and the segments select the visual shape.
When to use
Use
For switching between mutually-exclusive views of the same content — list vs grid view, day vs week vs month calendar, KB vs MB vs GB unit toggle, light vs dark vs auto theme. The user picks one; the underlying data stays the same and the segment selects how it is presented.
Avoid
For switching between different content panels — that is `Tabs`. For form values with form submission — that is `RadioGroup` (which is semantically identical but visually distinct as separate radio buttons). For binary on/off — that is `Switch` or `Toggle`. For more than 5 options — that is a `Select` dropdown.
Versus related
- tabs
`Tabs` switches between different *content* panels; `SegmentedControl` switches between *view-of-same- content*. The semantic distinction drives the role (`tablist` vs `radiogroup`) and the keyboard model (Tabs may use manual activation; SegmentedControl always uses auto-activation per APG radiogroup).
Code anatomy
| Slot | Code slot | Semantic |
|---|---|---|
root | root | radiogroup |
segment | segment | radio |
icon | segment-icon | presentational-or-img |
label | segment-label | text |
indicator | indicator | presentational |
Variants, properties, states
Variants
Structurally different versions of the component.
standardicon-only Properties
The same component, parameterised.
| Property | Type |
|---|---|
size | sm | md | lg |
orientation | horizontal | vertical |
hasIndicator | boolean |
States
Browser/user-driven (interactive) vs. app-driven (data).
| Kind | States |
|---|---|
interactive | hoverfocus-visibleactivedisabled |
data | selected |
State transitions
| From | To | Trigger |
|---|---|---|
selected | selected | User activates a different segment via Enter / Space or via ArrowKey navigation in roving-tabindex mode (which auto-activates on focus per APG radiogroup contract). The previously-selected segment loses `aria-checked="true"`; the newly-selected segment gains it. The indicator (if present) animates to the new position. |
Cross-framework expression
| Framework | Structure mechanism | Variant mechanism |
|---|---|---|
| Web Components | A `<ui-segmented-control>` host with `<ui-segment>` children; root carries `role="radiogroup"`, segments carry `role="radio"` plus shared keyboard state machine | attributes (`variant="icon-only"`, `size="md"`, `orientation="horizontal"`, `has-indicator`); per-segment `selected` attribute reflects `aria-checked` |
| React | React Aria `useRadioGroup` plus `useRadio` (with custom styled segments); Radix does not ship a SegmentedControl primitive but ToggleGroup with `type="single"` is the closest match | props with class-variance-authority for variant / size; controlled or uncontrolled value (the selected segment id) |
| Angular (signals) | Angular Material `MatButtonToggleGroup` (with `multiple="false"`) provides the closest pattern; or Angular CDK `cdk-listbox` configured for radio semantics | input<'standard' | 'icon-only'>(); input<'horizontal' | 'vertical'>(); two-way binding `[(value)]` |
| Vue | Headless UI `<RadioGroup>` plus styled radio options; or Radix Vue ToggleGroup; Vuetify `v-btn-toggle` with `mandatory` prop | defineProps with literal-union types; `:variant`, `:orientation` props |
Events
selectedChange
Performance thresholds
maxSegmentssegment-count≥5segmentsAbove ~5 segments, the connected-segment visual breaks down — the control overflows narrow viewports, the roving-tabindex requires more ArrowKey presses, and the mutual-exclusion intent becomes ambiguous (users may perceive a list of buttons rather than a single choice). Above 5, redesign as Tabs (with overflow- scroll), a Select dropdown, or split into smaller controls.
Accessibility
| Slot | Accessibility hint | |
|---|---|---|
root | Apply `role="radiogroup"` plus an `aria-label` (or `aria-labelledby` referencing a heading near the control). The radiogroup is the canonical APG choice because segments are *mutually-exclusive choices about view*, not navigation between content. Tabs would be wrong — Tabs switch content, segments switch view-of- content. | |
segment | Each segment is `role="radio"` with `aria-checked` reflecting selection state. Roving tabindex — only the currently-checked segment has `tabindex="0"`, others have `tabindex="-1"`. Disabled segments use `aria-disabled` and stay focusable so SR users hear them. | |
icon | Decorative when paired with a visible label (`aria-hidden="true"`). For icon-only variant, the segment's `aria-label` carries the meaning ("List view", "Grid view") because the icon alone is not a label. | |
label | Plain text. When icon-only variant is used, the label is hidden visually but kept in the DOM (visually- hidden) for SR users — never replaced by `aria-label` without a visible alternative when the segment also renders text. | |
indicator | Decorative — `aria-hidden="true"`. Selection is communicated through `aria-checked` on the radio segments; the indicator is visual reinforcement. |
Accessibility acceptance
Keyboard walk
| Keys | Expected |
|---|---|
Tab | Focus enters the currently-checked segment (roving tabindex). Subsequent Tab leaves the SegmentedControl toward the next document focusable — does NOT cycle through the segments. |
ArrowLeft / ArrowRight (horizontal orientation) | Moves focus AND selection to the previous / next segment. `aria-checked` flips on the new segment; previous segment becomes unchecked. The indicator (if present) animates to the new position. Wraps from last to first and vice versa per APG canonical. |
ArrowUp / ArrowDown (vertical orientation) | Same as ArrowLeft / Right but for vertical orientation. |
Home / End | Moves focus AND selection to the first / last segment respectively. Works in both orientations. |
Enter or Space (focus on already-checked segment) | No-op (segment is already selected). For unchecked segments, Enter / Space is equivalent to ArrowKey activation — moves selection here. |
Screen-reader announcements
| Trigger | Expected |
|---|---|
| Focus enters a segment | SR announces "<segment label>, radio button, checked, X of N" for the checked segment, or "not checked, X of N" for unchecked. The radiogroup's accessible name (from `aria-label` / `aria-labelledby`) is announced on first entry. |
| ArrowKey changes selection | SR announces the new segment's label and "checked" state. Driven by `aria-checked` flipping on the focused segment. |
| Indicator animates | No SR announcement — the indicator is decorative. |
axe-core rules to assert
aria-allowed-rolearia-required-attraria-required-childrenaria-required-parentcolor-contrastrole-img-alt
Common mistakes
#segmented-as-tabs
Used as content-switching tabs
The control is used to switch between different content panels (a settings page with "Account / Billing / Notifications" sections). Semantically this is Tabs; the `role="radiogroup"` of SegmentedControl is wrong here — SR users hear "radio button" instead of "tab", and panel content does not get the `role="tabpanel"` relationship.
Use Tabs for content-switching. SegmentedControl is for view-of-same-content (list vs grid). The decision test: does activating a segment change *what content is shown* or *how the same content is displayed*? "What" → Tabs; "how" → SegmentedControl.
#segmented-no-roving-tabindex
Every segment has tabindex="0"
Tab cycles through every segment one by one. APG canonical for radiogroup is roving tabindex — only the currently-checked segment has `tabindex="0"`, others have `tabindex="-1"`. ArrowKeys navigate within the group.
Roving tabindex — only the checked segment is in the tab order. ArrowKeys move focus AND selection (per APG radiogroup auto-activation). Tab from the radiogroup moves to the next document focusable, not to the next segment.
#segmented-arrow-no-activate
ArrowKeys move focus but do not activate
Implementation copies the manual-activation pattern from Tabs. ArrowKeys move focus between segments; the user must press Enter / Space to activate. APG radiogroup canonical is auto-activation — ArrowKey IS the activation.
ArrowKey moves focus AND activates the new segment. `aria-checked` flips immediately. The indicator (if present) animates to the new position. Enter / Space are no-ops on the focused segment when it is already checked (or are equivalent to ArrowKey activation if a rare focus-without-check edge case exists).
#segmented-color-only-selected
Selection differentiated by colour alone
Selected segment has a filled background; unselected segments have transparent background. The differentiator is colour. Colour-vision deficient users cannot distinguish; SR users get only the `aria-checked` cue.
Pair colour with non-colour cue: contrast-text foreground change (selected segment uses primary text colour vs muted; unselected uses muted), optional weight change, optional inline checkmark for icon-only-variant. Colour is reinforcement, not the sole signal.
#segmented-overflow-silent
Overflowing segments clipped at the inline-end
Five-segment control rendered in a narrow container; the last two segments clip at the edge. Users cannot see them; SR users navigate to them via ArrowKey but pointer users miss their existence.
For SegmentedControl above 5 segments, redesign as tabs with overflow-scroll, a Select, or splitting into smaller controls. Document the canonical maximum segment count in performance. Above the threshold, SegmentedControl is the wrong pattern.
Figma↔Code mismatches
- 01 Figma
SegmentedControl drawn as Tabs with no visual differentiation
CodeA SegmentedControl with `role="radiogroup"` rather than `role="tablist"`
ConsequenceDesigners may use SegmentedControl visually for what is semantically Tabs (switching between different content panels). Implementations following the design ship `role="radiogroup"` for content-switching, breaking the keyboard model (radiogroup uses ArrowKeys with auto-activation; tablist may use manual activation) and misleading SR users about the surface's purpose.
CorrectDistinguish at the canonical level: SegmentedControl switches view-of-same-content (list vs grid; day vs week vs month); Tabs switches between different content panels. The semantic distinction drives the role (`radiogroup` vs `tablist`). Document the boundary clearly in `whenToUse.vsRelated`.
- 02 Figma
Multi-select segmented control drawn (multiple segments highlighted)
CodeA canonical SegmentedControl is single-select only; multi-select segments are not segmented control
ConsequenceDesigners compose mocks with two or three segments "active" simultaneously. Implementations following the design ship multi-select; the semantic surface is neither radiogroup (single) nor a clean checkbox group (no visible grouping). The control becomes ambiguous.
CorrectSegmentedControl is single-select by canon — `role="radio"` semantics enforce mutual exclusion. For multi-select visual toggles, use a horizontal `<CheckboxGroup>` styled to look segmented; document this as the related pattern, not as a SegmentedControl variant.
- 03 Figma
Selected segment differentiated by a coloured fill alone
CodeSelected segment differentiated by fill PLUS contrast-text PLUS optional border
ConsequenceDesigners may use a single colour change to mark selection. Implementations following the design fail WCAG 1.4.1 (Use of Color) for users with colour-vision deficiencies; SR users hear the same announcement for every segment because the differentiation is purely visual.
CorrectTriple-layer selection differentiation: visual (background + foreground contrast + optional weight or border), accessible state (`aria-checked="true"`), and iconographic (an inline checkmark or selected-state icon for icon-only variants). The `aria-checked` is the source of truth; visuals reinforce.
- 04 Figma
Vertical segmented control drawn but no orientation property documented
CodeA SegmentedControl with an `orientation` property switching between row and column layouts
ConsequenceDesigners compose vertical-segmented variants in mocks without flagging that the orientation is property-driven. Developers ship one orientation; the design is occasionally requested in the other and re-implemented from scratch.
CorrectDocument `orientation` as a first-class property (horizontal default, vertical opt-in). The keyboard model adapts: ArrowLeft / Right for horizontal, ArrowUp / Down for vertical, with Home / End working in both.