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

Highlight
Fig 1.1 · Segmented Control · Dev view
Dev

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
Both

Variants, properties, states

Variants

Structurally different versions of the component.

standardicon-only

Properties

The same component, parameterised.

PropertyType
sizesm | md | lg
orientationhorizontal | vertical
hasIndicatorboolean

States

Browser/user-driven (interactive) vs. app-driven (data).

KindStates
interactive
hoverfocus-visibleactivedisabled
data
selected
Both

State transitions

FromToTrigger
selectedselectedUser 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.
Dev

Cross-framework expression

FrameworkStructure mechanismVariant 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
Both

Events

  1. selectedChange
    Payload
    `{ value: string }` — the id of the newly selected segment. Always a string matching one of the rendered segment ids; never empty (segmented controls always have one segment selected — the canonical default requires `mandatory` selection).
    Web Components
    `selectedChange` CustomEvent on the host with `event.detail = { value }`.
    React
    `onValueChange(value: string)` controlled-pattern callback (Radix ToggleGroup, React Aria `useRadioGroup`).
    Angular Signals
    `output<string>('selectedChange')`; pair with `[(value)]` for two-way binding.
    Vue
    `@update:modelValue` for `v-model`.
Dev

Performance thresholds

  • maxSegmentssegment-count5segments

    Above ~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.

Both

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

Accessibility acceptance

Keyboard walk

KeysExpected
TabFocus 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 / EndMoves 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

TriggerExpected
Focus enters a segmentSR 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 selectionSR announces the new segment's label and "checked" state. Driven by `aria-checked` flipping on the focused segment.
Indicator animatesNo SR announcement — the indicator is decorative.

axe-core rules to assert

  • aria-allowed-role
  • aria-required-attr
  • aria-required-children
  • aria-required-parent
  • color-contrast
  • role-img-alt
Dev

Common mistakes

#segmented-as-tabs

Used as content-switching tabs

Problem

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.

Fix

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"

Problem

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.

Fix

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

Problem

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.

Fix

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

Problem

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.

Fix

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

Problem

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.

Fix

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
  1. 01
    Figma

    SegmentedControl drawn as Tabs with no visual differentiation

    Code

    A SegmentedControl with `role="radiogroup"` rather than `role="tablist"`

    Consequence

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

    Correct

    Distinguish 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`.

  2. 02
    Figma

    Multi-select segmented control drawn (multiple segments highlighted)

    Code

    A canonical SegmentedControl is single-select only; multi-select segments are not segmented control

    Consequence

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

    Correct

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

  3. 03
    Figma

    Selected segment differentiated by a coloured fill alone

    Code

    Selected segment differentiated by fill PLUS contrast-text PLUS optional border

    Consequence

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

    Correct

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

  4. 04
    Figma

    Vertical segmented control drawn but no orientation property documented

    Code

    A SegmentedControl with an `orientation` property switching between row and column layouts

    Consequence

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

    Correct

    Document `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.