Segmented Control
Examples
Default
Exclusive selection in a compact track. Defaults to Sm (36px) — the Figma default size set.
Sizes
Figma size sets: Sm 36px (default) · Md 40px · Lg 44px.
Icons
Icon-only and text+icon segments. Icon-only items require aria-label.
Fluid
fluid stretches the control and gives segments equal width.
Disabled
Disable the whole control or individual segments.
TanStack Form
Bind with value / onValueChange / name — no adapters.
RTL
Layout uses logical properties — segment order and icon placement follow dir.
API Reference
Segmented
| Prop | Type | Default | Description |
|---|---|---|---|
items | SegmentedItem[] | — | Segments to render |
size | "sm" | "md" | "lg" | "sm" | Track height 36 / 40 / 44 |
fluid | boolean | false | Equal-width segments that fill the control |
value | string | — | Controlled selected value |
defaultValue | string | first item | Uncontrolled initial value |
onValueChange | (value: string, eventDetails) => void | — | Fires when selection changes (never empty) |
disabled | boolean | false | Disables the whole control |
name | string | — | Hidden input name for native form posts |
className | string | — | Extra classes on the track |
id | string | — | Track id |
aria-label | string | — | Accessible name for the group |
aria-labelledby | string | — | Id of a visible label |
aria-describedby | string | — | Id of description / error text |
SegmentedItem
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | — | Unique value within the control |
label | ReactNode | — | Segment text (omit for icon-only) |
icon | ReactNode | — | Inline-start icon |
disabled | boolean | false | Disables this segment only |
aria-label | string | — | Required for icon-only segments |
Dropdown segments (SegCtrl → Dropdown in Figma) are out of scope for this release.
Accessibility
WCAG success criteria
| Criterion | Level | How EDS helps | |
|---|---|---|---|
| 2.1.1 Keyboard | A | Full keyboard selection via Tab, arrows, Space/Enter | |
| 2.4.7 Focus Visible | AA | Distinct focus-visible ring on segments | |
| 4.1.2 Name, Role, Value | A | Group naming + aria-pressed value; icon-only names via item aria-label | |
| 1.3.1 Info and Relationships | A | Group/label association through aria-labelledby / aria-label | |
| 1.4.3 Contrast (Minimum) | AA | Text and icons use semantic foreground tokens against track/selected surfaces | |
| 1.4.11 Non-text Contrast | AA | Selected border, focus ring, and control boundary use outline/adaptive tokens |
Why it matters
Segmented controls look like tabs but behave like an exclusive choice — the wrong pattern leaves keyboard and screen-reader users guessing whether they can leave a view empty, how focus moves, and what each icon means. A clear group name, pressed state, and arrow-key navigation keep dense filters and view switchers usable for everyone, including motor and cognitive impairments.
Keyboard
| Key | Behavior |
|---|---|
| ⇥ Tab / ⇧ + ⇥ Tab | Moves focus into / out of the control (roving tabindex among segments) |
| ← / → | Moves focus between segments (respects dir; loops at the ends) |
| ↑ / ↓ | Same as horizontal arrows for this control |
| Space / ↵ Enter | Selects the focused segment |
| Clicking the active segment | Selection is not cleared — one option always remains selected |
Disabled segments are skipped for activation. A fully disabled control is removed from the tab order as a group of disabled buttons.
Screen readers & semantics
Built on Base UI Toggle Group:
| Surface | Implementation |
|---|---|
| Group | role="group" — set aria-label or aria-labelledby on Segmented |
| Segment | Native <button> with aria-pressed for the selected state |
| Icon-only | Pass aria-label on the item; icon markup is aria-hidden |
| Form name | Optional hidden <input type="hidden"> mirrors the selected value |
Do not nest another interactive control inside a segment label. Prefer a visible <label id> + aria-labelledby over a redundant aria-label when the page already shows a heading for the control.
Focus & visuals
| Concern | EDS behavior |
|---|---|
| Focus ring | 2px outline-primary inset on the focused segment (Figma Focus) |
| Selected | bg-secondary + adaptive-20 border + shadow-xs lift |
| Hover | bg-tertiary-hover on unselected segments |
| Disabled | text-disabled; selected+disabled keeps the lift at reduced opacity |
| RTL | Logical padding/gap; DOM order + dir drive reading and arrow keys |
| Contrast | Token-mapped track (adaptive-8) and selected surface (bg-secondary) |
Build with it
| Do | Don’t | ||
|---|---|---|---|
Give the control an aria-label or aria-labelledby | Ship an unlabeled group of identical “Label” buttons | ||
Set aria-label on every icon-only item | Rely on decorative SVG alone for the accessible name | ||
| Use for exclusive, same-level choices (views, ranges) | Use as navigation tabs that change URL without selection semantics | ||
Bind value / onValueChange for TanStack Form | Invent a wrapper that remaps pressed arrays | ||
| Disable a segment when the option is unavailable | Hide the only remaining valid option and leave selection empty |
Quick test
- Keyboard — Tab to the control, arrow across segments, confirm Space/Enter selects and the previous segment unpresses. Click the active segment and confirm it stays selected.
- Screen reader — With VoiceOver/NVDA, verify the group name and that the selected segment announces as pressed.
- Icon-only — Navigate an icon-only control and confirm each segment has a spoken name.
- RTL — Open the RTL example; confirm order, icons, and arrow keys follow
dir="rtl". - Form — Submit the TanStack Form demo empty (error), then with a selection (success).