---
title: "Segmented button"
description: "Lets people choose one option, or several, from two to five related options drawn side by side as the joined segments of one control."
url: "https://tanrei.chnotchy.com/components/segmented-button/"
last_updated: 2026-10-23T21:17:12.000Z
---

# Segmented button

> Lets people choose one option, or several, from two to five related options drawn side by side as the joined segments of one control.

## Overview

### Usage

A segmented button holds its segments the way each platform holds repeated children: child elements on the web, views in a view builder on SwiftUI, calls in a content lambda on Compose, a `segments` list on Flutter and an `items` array on React Native. The selection belongs to the set, by value. `selection` says whether it is one value or several.

**Web**

```html
<tanrei-segmented-button label="Range" value="week">
  <tanrei-segment value="day" label="Day"></tanrei-segment>
  <tanrei-segment value="week" label="Week"></tanrei-segment>
  <tanrei-segment value="month" label="Month"></tanrei-segment>
</tanrei-segmented-button>

<!-- Several at once. values is a property (an array); the attribute takes a JSON array or space-separated values. -->
<tanrei-segmented-button id="days" selection="multiple" size="small" label="Days" values="mon tue">
  <tanrei-segment value="mon" label="Mon"></tanrei-segment>
  <tanrei-segment value="tue" label="Tue"></tanrei-segment>
  <tanrei-segment value="wed" label="Wed" disabled></tanrei-segment>
</tanrei-segmented-button>

<script type="module">
  import '@tanrei/web/components/segmented-button';
  // The element updates its own value or values, then fires change.
  document.querySelector('[label="Range"]').addEventListener('change', (e) => showRange(e.detail.value));
  document.querySelector('#days').addEventListener('change', (e) => setDays(e.detail.values));
</script>
```

**React**

```tsx
// The default system leaves the segmented button out: it is in the entries of m3-like, m3-expressive-like and cupertino, and in /all.
import { Segment, SegmentedButton } from '@tanrei/react/m3-like';

// detail is { value } for a single choice and { values } for several.
<SegmentedButton label="Range" value={range} onChange={(e) => 'value' in e.detail && setRange(e.detail.value)}>
  <Segment value="day" label="Day" />
  <Segment value="week" label="Week" />
  <Segment value="month" label="Month" />
</SegmentedButton>

<SegmentedButton selection="multiple" label="Days" values={days} onChange={(e) => 'values' in e.detail && setDays(e.detail.values)}>
  <Segment value="mon" label="Mon" />
  <Segment value="tue" label="Tue" />
  <Segment value="wed" label="Wed" disabled />
</SegmentedButton>
```

**SwiftUI**

```swift
import Tanrei

// The binding's type is the selection: an optional value for one, a Set for several.
// Apply .tag and .labelStyle to the TanreiSegment itself, before any other modifier.
TanreiSegmentedButton("Range", selection: $range) {
    TanreiSegment("Day").tag("day")
    TanreiSegment("Week").tag("week")
    TanreiSegment("Month").tag("month")
}

// .labelStyle(.iconOnly) is labelHidden: the title still names the segment.
TanreiSegmentedButton("Text alignment", selection: $alignment) {
    TanreiSegment("Align left", icon: "format_align_left").tag("start").labelStyle(.iconOnly)
    TanreiSegment("Center", icon: "format_align_center").tag("center").labelStyle(.iconOnly)
    TanreiSegment("Align right", icon: "format_align_right").tag("end").labelStyle(.iconOnly)
}
```

**Compose**

```kotlin
import io.github.chnotchy.tanrei.components.TanreiSegment
import io.github.chnotchy.tanrei.components.TanreiSegmentedButton
import io.github.chnotchy.tanrei.components.TanreiSegmentedButtonSelectionMode
import io.github.chnotchy.tanrei.primitives.TanreiText
import io.github.chnotchy.tanrei.tokens.TanreiSegmentedButtonSize

// One callback for both selections. A single-choice set passes a set of one.
TanreiSegmentedButton(
    onSelectionChange = { range = it.single() },
    contentDescription = "Range",
    selectedValue = range,
) {
    TanreiSegment(value = "day", label = "Day")
    TanreiSegment(value = "week", label = "Week")
    TanreiSegment(value = "month", label = "Month")
}

TanreiSegmentedButton(
    onSelectionChange = { days = it },
    contentDescription = "Days",
    selectionMode = TanreiSegmentedButtonSelectionMode.Multiple,
    selectedValues = days,
    size = TanreiSegmentedButtonSize.Small,
) {
    TanreiSegment(value = "mon", label = "Mon")
    TanreiSegment(value = "tue", label = "Tue")
    // Compose uses the inverse flag.
    TanreiSegment(value = "wed", label = "Wed", enabled = false)
}

// labelHidden draws the icon alone. The label still names the segment.
TanreiSegment(value = "start", label = "Align left", labelHidden = true, icon = { TanreiText("format_align_left") })
```

**Flutter**

```dart
import 'package:tanrei/tanrei.dart';

// Shaped like SegmentedButton: one Set holds the selection, at most one member while single.
TanreiSegmentedButton<String>(
  semanticLabel: 'Range',
  selected: <String>{range},
  onSelectionChanged: (Set<String> value) => setState(() => range = value.single),
  segments: const <TanreiSegment<String>>[
    TanreiSegment<String>(value: 'day', label: 'Day'),
    TanreiSegment<String>(value: 'week', label: 'Week'),
    TanreiSegment<String>(value: 'month', label: 'Month'),
  ],
);

TanreiSegmentedButton<String>(
  semanticLabel: 'Days',
  multiSelectionEnabled: true,
  size: TanreiSegmentedButtonSize.small,
  selected: days,
  onSelectionChanged: (Set<String> value) => setState(() => days = value),
  segments: const <TanreiSegment<String>>[
    TanreiSegment<String>(value: 'mon', label: 'Mon'),
    TanreiSegment<String>(value: 'tue', label: 'Tue'),
    TanreiSegment<String>(value: 'wed', label: 'Wed', enabled: false),
  ],
);

// labelHidden draws the icon alone. The label still names the segment.
const TanreiSegment<String>(value: 'start', label: 'Align left', icon: Icon(AppSymbols.formatAlignLeft), labelHidden: true);
```

**React Native**

```tsx
import { SegmentedButton } from '@tanrei/react-native';

// Controlled: onSelectionChange receives the value (single) or the values (multiple).
<SegmentedButton
  accessibilityLabel="Range"
  value={range}
  onSelectionChange={setRange}
  items={[
    { value: 'day', label: 'Day' },
    { value: 'week', label: 'Week' },
    { value: 'month', label: 'Month' },
  ]}
/>

<SegmentedButton
  selectionMode="multiple"
  accessibilityLabel="Days"
  size="small"
  values={days}
  onSelectionChange={setDays}
  items={[
    { value: 'mon', label: 'Mon' },
    { value: 'tue', label: 'Tue' },
    { value: 'wed', label: 'Wed', disabled: true },
  ]}
/>

// An icon-only segment: { value: 'start', label: 'Align left', icon: 'format_align_left', labelHidden: true }
```

## Specs

- Category: Actions
- Level: base
- Listing: default
- Page: https://tanrei.chnotchy.com/components/segmented-button/
- Spec: https://github.com/chnotchy/tanrei/blob/main/specs/segmented-button.spec.json

### Availability

| Design system | Has it | Notes |
| --- | --- | --- |
| Tanrei (`tanrei`) | No | Left out by the `tanrei` layer. Instead use: Tabs, Radio button, Button group. One choice among several is told by what it changes: tabs where it switches the content below, radio buttons where it sets a value, a button group where each button toggles on its own. |
| Cupertino (Apple HIG-inspired) (`cupertino`) | Yes | — |
| M3 Expressive-like (Material 3 Expressive-inspired) (`m3-expressive-like`) | Yes | — |
| M3-like (Material 3-inspired) (`m3-like`) | Yes | — |

### Platforms

- Web: the element `<tanrei-segmented-button>`, registered by importing `@tanrei/web/cupertino` (or only its own module, `@tanrei/web/components/segmented-button`).
- React: `SegmentedButton` from `@tanrei/react/cupertino`.

| Platform | Name | Status |
| --- | --- | --- |
| Web | `tanrei-segmented-button` | lagging |
| SwiftUI | `TanreiSegmentedButton` | lagging |
| Jetpack Compose | `TanreiSegmentedButton` | lagging |
| Flutter | `TanreiSegmentedButton` | lagging |
| React Native | `SegmentedButton` | lagging |

Names a platform gives in its own idiom:

| Spec | Web | SwiftUI | Jetpack Compose | Flutter | React Native |
| --- | --- | --- | --- | --- | --- |
| `selection` (prop) | `selection` | `selection` | `selectionMode` | `multiSelectionEnabled` | `selectionMode` |
| `value` (prop) | `value` | `selection` | `selectedValue` | `selected` | `value` |
| `values` (prop) | `values` | `selection` | `selectedValues` | `selected` | `values` |
| `label` (prop) | `label` | `label` | `contentDescription` | `semanticLabel` | `accessibilityLabel` |
| `change` (event) | `change` | `selection` | `onSelectionChange` | `onSelectionChanged` | `onSelectionChange` |

Statuses: `planned` — In the inventory. Nothing shippable exists yet. `lagging` — An implementation of an earlier spec exists; it has not caught up with the current one. `alpha` — Implemented with tests. The API may still change. `beta` — Feature complete and at parity. Only fixes are expected. `stable` — Covered by semantic versioning. `deprecated` — Still shipped; scheduled for removal.

### Anatomy

- `set`: The row that holds the segments: its fill, its outline and its outer corners. It gives the segments equal widths, draws the dividers between them and moves keyboard focus among them.
- `container`: One segment: the area that carries hover, focus and press, its selected fill, and its hit area.
- `check` (optional): The check a selected segment shows before its label, in place of its icon when it has one: the glyph check of the system's icon set, drawn with the icon set's family, fill, weight and optical size. The segmented button draws no mark of its own. Not drawn in an icon-only segment, nor where the system's check size is 0.
- `icon` (optional): A glyph before the label, or alone in an icon-only segment.
- `label`: The segment's text and its accessible name. It may be hidden for an icon-only segment (labelHidden) and still name it.
- `divider`: The line between two neighbouring segments.
- `outline`: The border around the whole set, drawn inside its edge.
- `focus-ring`: Gap-then-ring indicator shown for keyboard focus, drawn inside the focused segment.

### Props

| Prop | Type | Default | Web attribute | Description |
| --- | --- | --- | --- | --- |
| `selection` | `enum`: `single`, `multiple` | `"single"` | `selection` | single lets people choose one option at a time, as a radio group does; multiple lets them switch each option on and off. |
| `value` | `string` | — | `value` | The value of the selected segment while selection is single; null selects none. The segmented button sets it when a segment is activated and emits change; the product may also set it. |
| `values` | `strings` | `` | `values` | The values of the selected segments while selection is multiple: a list of strings (the platforms take a set). An empty list selects none. |
| `size` | `enum`: `small`, `medium`, `large` | `"medium"` | `size` | Visual height (32 / 36 / 40), matching Button's heights so that the set can share a row with buttons. The hit area always meets the minimum target. A design system may add values. |
| `label` | `string` | `""` | `label` | The accessible name of the set: the question its options answer ("Range", "Text alignment"). Required. |

### Events

| Event | Description |
| --- | --- |
| `change` | The user changed the selection; carries value (single) or values (multiple) after the change, which the set now holds. |

### Item: `segment`

One option of the set: a label, an optional icon, and the value that selects it.

Names: Web `tanrei-segment`, SwiftUI `TanreiSegment`, Jetpack Compose `TanreiSegment`, Flutter `TanreiSegment`, React Native `Segment`.

| Prop | Type | Default | Web attribute | Description |
| --- | --- | --- | --- | --- |
| `value` | `string` | `""` | `value` | Identifies the segment: the set's value or values name it by this, and change carries it. Unique within the set; required. |
| `label` | `string` | `""` | `label` | The segment's text and its accessible name. Required, even when labelHidden is true. |
| `icon` | `string` | `""` | `icon` | The name of an icon (a glyph of the system's icon set). Give every segment of a set an icon, or none. |
| `labelHidden` | `boolean` | `false` | `label-hidden` | Draws the icon alone and keeps label as the accessible name (an icon-only segment). |
| `disabled` | `boolean` | `false` | `disabled` | Prevents choosing or clearing the segment; the arrow keys skip it. |

### States

`enabled`, `disabled`, `hovered`, `focused`, `pressed`, `selected`

### Accessibility

- Role: `radiogroup`
- Labeling: The set is named by label: the question its options answer. Each segment is named by its own label, drawn or not (labelHidden). Icons, the check, the dividers and the outline are decorative: a segment's state is announced from its checked or pressed state, never from the check.
- Minimum target size: yes

| Keys | Action |
| --- | --- |
| Tab / Shift+Tab | Moves focus into the set (to the selected segment, the last focused one, or the first enabled one) and out of it. |
| Right Arrow / Down Arrow | Moves focus to the next enabled segment, wrapping; with single selection it also selects it (Left Arrow in right-to-left layouts). |
| Left Arrow / Up Arrow | Moves focus to the previous enabled segment, wrapping; with single selection it also selects it (Right Arrow in right-to-left layouts). |
| Home / End | Moves focus to the first or last enabled segment; with single selection it also selects it. |
| Enter or Space | Selects the focused segment (single) or switches it on or off (multiple). |

- Single selection is a radio group: arrow keys move and select at once, as the radio button and tabs do, because a segment shows its effect without a noticeable delay. Multiple selection is a group of toggle buttons with one Tab stop: arrow keys only move focus, and Space or Enter switches.
- Selection is the set's: value or values holds the selected segments' values, and change carries the new ones. A segment's disabled is its own.
- iOS has no radio role: a segment of a single-choice set is a button with the selected trait and its position ("2 of 3") in its accessibility value, as the radio button is. Compose has no radio group role; the set is a selectable group and each segment `Role.RadioButton` (single) or a toggleable `Role.Button` (multiple), as the filter chip is.
- A segmented button chooses among options of one setting. Actions belong to a button group; switching views of one subject belongs to tabs.
- Text size does not clip a segment: labels wrap and every segment grows to the tallest. The icon is not text and keeps its size.
- How a state looks is a value decision of the active design system; components read the state's tokens and never compute the effect themselves.

### Behaviour

Requirements in EARS form, each with its stable id.

- **SEG-1** WHEN the user activates an enabled segment by pointer, touch, Enter or Space WHILE selection is single and value is not that segment's value THE segmented button SHALL set value to the segment's value, so that the segment that was selected becomes unselected, and emit change exactly once with it; WHEN the segment is already selected THE segmented button SHALL change nothing and emit nothing, because a single choice is never cleared by its own segment.
- **SEG-2** WHEN the user activates an enabled segment by pointer, touch, Enter or Space WHILE selection is multiple THE segmented button SHALL add the segment's value to values, or remove it when values holds it, and emit change exactly once with the new values; values may become empty.
- **SEG-3** WHILE a segment's disabled is true THE segmented button SHALL NOT select or clear it or emit change for it, SHALL skip it with the arrow keys, SHALL NOT make it the Tab stop and SHALL expose the disabled state to assistive technology.
- **SEG-4** WHILE a segment is hovered, keyboard-focused or pressed THE segmented button SHALL render its container, label, icon, check, elevation and opacity from the component tokens of its condition (selected or unselected) in the highest-precedence active state, with precedence pressed over focused over hovered; focus gained by pointer or touch SHALL NOT select the focused state. A part the interaction states recolor changes over the component's state motion; only a change of condition uses the selection motion (SEG-16).
- **SEG-5** WHEN a segment receives keyboard focus THE segmented button SHALL show the focus ring inside the segment's container, where the set's outline and the neighbouring segments cannot cover or clip it, without animation; focus gained by pointer or touch SHALL NOT show it.
- **SEG-6** WHERE the set is shorter than the minimum target THE segmented button SHALL extend each segment's hit area above and below the set to sys.target.touch for touch input and sys.target.pointer for fine pointers without changing its visual size, and SHALL make each segment at least as wide as that target, because neighbouring segments cannot extend a hit area over each other; a platform that cannot tell the input type SHALL use sys.target.touch.
- **SEG-7** WHILE a segment's disabled is true THE segmented button SHALL render its container, label, icon, check, elevation and opacity from the disabled component tokens of its condition; the set's outline and the dividers SHALL keep their colors, so that the set keeps a visible boundary.
- **SEG-8** WHEN size names a value that the active design system does not have THE segmented button SHALL render with the default value's tokens, and in debug builds report the unknown value.
- **SEG-9** WHEN the segment is pressed by pointer, touch, Enter or Space THE segment SHALL show the pressed state without delay and SHALL keep it visible for at least sys.motion.duration.press-hold, even when the press ends sooner.
- **SEG-10** WHILE pressed THE segment SHALL answer as sys.behavior.pressed-feedback names: overlay applies the pressed tokens to the whole container at once; ripple grows the pressed state layer from the point of contact (from the center for keyboard activation); dim applies the pressed opacity to the whole control and draws no state layer; none draws no answer to the press. Under ripple and dim only the container color stays with the state under the press; label, icon, check, elevation, opacity and shape still come from the pressed tokens. Under none every part stays with the state under the press. The ripple grows over comp.segmented-button.motion.feedback-duration with comp.segmented-button.motion.state-easing, fades over the state duration of its size (comp.segmented-button.motion.state-duration.small, .medium or .large) with comp.segmented-button.motion.state-easing once the press has ended and press-hold has passed, and appears whole where motion is reduced.
- **SEG-11** THE segment SHALL ease its container corners by sys.shape.corner-smoothing (0 is a quarter circle, 0.6 the iOS continuous corner; a platform with only the OS curve draws it for any amount above 0) and show its keyboard focus as sys.behavior.focus-style names (ring or glow); a platform that cannot draw a choice SHALL fall back to the first value and state that in its README.
- **SEG-12** THE segmented button SHALL render a segment from the selected condition (comp.segmented-button.selected.container.color and the other selected parts) WHILE selection is single and value equals the segment's value, or selection is multiple and values holds it, and every other segment from the unselected condition (comp.segmented-button.unselected.container.color and so on); WHEN value or values names no segment THE segmented button SHALL select nothing for that name, and WHEN a segment's value is empty or repeats another segment's THE segmented button SHALL still render it and treat the first segment with a value as the one that value selects; in debug builds it SHALL report each of these.
- **SEG-13** THE segmented button SHALL lay its segments out in one row, in order, in a set at least the container height of its size tall (comp.segmented-button.container.height.medium for medium, and so on), filled with comp.segmented-button.set.color, with outer corners of comp.segmented-button.set.shape, an outline of comp.segmented-button.outline.width in comp.segmented-button.outline.color inside its edge, and its segments inset from that edge by comp.segmented-button.set.padding; the outer corners of the first and last segment SHALL be the set's corners less set.padding, so that the two curves stay concentric, and every other corner of a segment SHALL be comp.segmented-button.container.shape; each segment SHALL clip its fill, state layer and ripple to its own shape, and the set SHALL NOT clip a segment's elevation.
- **SEG-14** THE segmented button SHALL give every segment the same width: the widest segment's content plus the padding of its size on each side (comp.segmented-button.container.padding-inline.medium for medium, and so on), a segment's content being its leading glyph (its icon, or the check where one would be drawn), the gap of its size (comp.segmented-button.container.gap.medium, and so on) and its label, measured as if the segment were selected, so that selecting a segment never changes a width; WHERE the layout gives the set more width THE segmented button SHALL divide it equally among the segments, and WHERE less, it SHALL divide what it has equally and let labels wrap onto more lines rather than truncate, growing every segment to the tallest, with at least the padding-block of its size (comp.segmented-button.container.padding-block.medium, and so on) above and below.
- **SEG-15** THE segmented button SHALL centre a segment's content in it, the leading glyph first; SHALL set the label in the label style of its size (comp.segmented-button.label.typescale.medium, and so on) and the label color of its condition and state; and SHALL draw the icon in the icon color of its condition and state at the icon size of its size (comp.segmented-button.icon.size.medium, and so on) through the icon helper, with the family and fill of the segment's condition (selected for a selected segment, otherwise unselected), sys.icon.weight and an optical size clamped between sys.icon.optical-size.min and sys.icon.optical-size.max.
- **SEG-16** WHILE a segment is selected and its label is drawn THE segmented button SHALL draw the glyph check of the system's icon set in the leading position, in place of the segment's icon when it has one, as the filter chip draws its check (CHP-13, CHP-14), at the check size of its size (comp.segmented-button.check.size.medium, and so on) in the icon color of the selected condition, with sys.icon.family.selected, sys.icon.fill.selected, the weight comp.segmented-button.check.weight (not sys.icon.weight: the check is a control's own mark drawn on the selected fill, as the checkbox's is, CHK-12) and an optical size clamped between sys.icon.optical-size.min and sys.icon.optical-size.max, and SHALL NOT mirror it when the layout direction is right-to-left; the weight SHALL change the stroke of the glyph only, the check's box staying its check size; THE segmented button SHALL draw no mark of its own (no path, shape or character of its own for the check). An icon-only segment SHALL keep its icon when selected and draw no check; WHILE the check size of the segment's size is 0 THE segmented button SHALL draw no check, reserve no room for it, and keep the icon. WHEN a segment becomes selected or unselected THE segmented button SHALL fade the check in or out (a glyph is not drawn along a path), cross-fading it with the icon where the segment has one, and change the segment's colors and elevation over comp.segmented-button.motion.selection-duration with comp.segmented-button.motion.selection-easing, re-centring the content as the check comes and goes, and SHALL show the new condition at once where motion is reduced.
- **SEG-17** THE segmented button SHALL draw a line of comp.segmented-button.divider.width in comp.segmented-button.divider.color between each two neighbouring segments, taking that width between them and running from the set's top padding to its bottom padding; the line SHALL keep its color and full opacity whatever the state or condition of either segment, and SHALL be hidden from assistive technology.
- **SEG-18** WHILE selection is single THE segmented button SHALL be one Tab stop — the selected segment, or the first enabled segment while none is selected; WHILE a segment has keyboard focus Right Arrow and Down Arrow SHALL move focus to the next enabled segment and Left Arrow and Up Arrow to the previous one, wrapping at the ends, and Home and End to the first and last enabled segment, and each move SHALL also select the segment it reaches and emit change, as a radio group does; in a right-to-left layout Left Arrow SHALL move to the next segment and Right Arrow to the previous.
- **SEG-19** WHILE selection is multiple THE segmented button SHALL be one Tab stop — the segment that had focus last, or else the first selected segment, or else the first enabled segment; WHILE a segment has keyboard focus Right Arrow and Down Arrow SHALL move focus to the next enabled segment and Left Arrow and Up Arrow to the previous one, wrapping at the ends, and Home and End to the first and last enabled segment, without changing the selection, and Space or Enter SHALL switch the focused segment (SEG-2); in a right-to-left layout Left Arrow SHALL move to the next segment and Right Arrow to the previous.
- **SEG-20** WHILE selection is single THE segmented button SHALL expose the set as a radio group named by label and each segment as a radio with its checked state; WHILE selection is multiple THE segmented button SHALL expose the set as a group named by label and each segment as a toggle button with its pressed state, as a toggle Button is; in both, each segment SHALL be named by its own label whether or not it is drawn, over a name the product gives the segment on the element (aria-label or aria-labelledby) and expose its position and the number of segments, and the icons, the check, the dividers and the outline SHALL be hidden.
- **SEG-21** WHEN the layout direction is right-to-left THE segmented button SHALL order the segments from the right, mirror each segment's content (the glyph to the right of the label) and apply its paddings by logical start and end, and SHALL grow a ripple from the point of contact as in left-to-right.
- **SEG-22** WHILE a segment's label is empty and the product gives the segment a name of its own on the element — aria-label, or aria-labelledby naming an element — THE segmented button SHALL expose that name as the segment's accessible name; WHILE a segment's label is empty and the product gives it none THE segmented button SHALL still render the segment, and in debug builds report that it has no accessible name; WHILE a segment's labelHidden is true and its icon is empty THE segmented button SHALL draw the label anyway and in debug builds report the segment; WHILE the set's label is empty, or the set holds fewer than two or more than five segments, THE segmented button SHALL still render and in debug builds report it.
- **SEG-23** WHEN the colors or the elevation of a segment change between interaction states THE segmented button SHALL animate them over the state duration of its size (comp.segmented-button.motion.state-duration.small, .medium or .large) with comp.segmented-button.motion.state-easing, and SHALL change them at once where motion is reduced.
- **SEG-24** WHILE a segment is selected THE segmented button SHALL cast the elevation of its condition and state (comp.segmented-button.selected.container.elevation at rest, and so on) outside the segment and above its neighbours; THE segmented button SHALL NOT position itself or draw anything outside the set but that elevation.
- **SEG-25** IF the icon set cannot draw the check yet (its font is still loading or failed to load) THE segmented button SHALL draw nothing in the check's place, never the glyph's name as text, SHALL keep the room SEG-14 measures for it so that no width changes, SHALL still expose the segment's selected state, and SHALL show the check once the icon set can draw it, without the selection motion of SEG-16.
- **SEG-26** THE segmented button SHALL take its layout direction from where it is used — the direction of the nearest ancestor that sets one, and the platform's own where none does — and SHALL have no direction setting of its own; WHEN that direction changes while the segmented button is shown THE segmented button SHALL draw itself for the new direction at once, without a motion of its own, keeping its selection and its focus.
- **SEG-27** WHERE the layout direction is right-to-left THE segmented button SHALL draw a segment's icon mirrored left to right exactly when the system's icon set marks that glyph's name as mirrored or the product forces mirroring for that use, and unmirrored when the product forbids it: the icon helper mirrors the glyph inside its own box, about the box's vertical centre line, changing neither its place, size, color and fill nor the order SEG-21 gives. THE segmented button SHALL NOT mirror a glyph itself, and SHALL NOT mirror content the product gives as an image or a view of its own rather than by a glyph's name. The check of a selected segment is never mirrored (SEG-16).
- **SEG-28** THE segmented button SHALL lay out a segment's label as text of its own direction, isolated from the text around it: each takes the base direction of its first strong character, or the layout direction where it has none, so that a left-to-right word, a number with its sign or unit, a telephone number and an address keep their own order in a right-to-left layout and reorder nothing beside them; THE segmented button SHALL still place each label and align its lines by the layout direction, not by the text's own.
- **SEG-29** WHERE the platform has ARIA attributes (the web), WHEN the product sets an aria- attribute on a segment's host, by the attribute or by the ARIA property that reflects it, THE segmented button SHALL move it to the data-aria- attribute of the same name, so that the host is no second node with its name, description or state, and SHALL expose it on the element that carries the segment's role — an id reference by the elements it names in the host's tree or a tree around it, an error message as part of the description while the segment is invalid — except those a segment owns (aria-checked, aria-pressed, aria-disabled), which it SHALL move and not expose. WHEN the product removes the aria- attribute or its data-aria- twin, or sets the ARIA property that reflects it (ariaLabel, ariaDescribedByElements…) to null, THE segmented button SHALL take it off that element. WHEN the product sets an aria- attribute a segment owns THE segmented button SHALL report it in debug builds. The set's host carries its radio group or group role itself (SEG-20), so what the product writes on it is not moved, but for a name: WHILE the set's label is not empty THE segmented button SHALL hold the product's aria-label and aria-labelledby on that host as their data-aria- twins, so that label is the set's whole name although the platform gives aria-labelledby the precedence, and WHEN label is emptied SHALL give them back as they were written; WHILE it is empty a name the product gives so is the set's name and SHALL NOT be reported as having none in debug builds (SEG-22); WHEN the product removes one, or its data-aria- twin, or sets the ARIA property that reflects it to null, THE segmented button SHALL take it away.

### Tokens

As written in the token documents (DTCG): an alias in braces names the token it takes its value from, which each design system resolves. On the web each token is a CSS custom property: `--tanrei` and the name after `tanrei`, joined by hyphens (`tanrei.comp.segmented-button.…` → `--tanrei-comp-segmented-button-…`).

#### `tanrei.comp.segmented-button`

| Token | Type | Value |
| --- | --- | --- |
| `comp.segmented-button.set.color` | `color` | `{"colorSpace":"srgb","components":[0,0,0],"alpha":0,"hex":"#000000"}` |
| `comp.segmented-button.set.shape` | `dimension` | `{tanrei.sys.shape.corner.small}` |
| `comp.segmented-button.set.padding` | `dimension` | `{tanrei.sys.space.space0}` |
| `comp.segmented-button.outline.color` | `color` | `{tanrei.sys.color.outline}` |
| `comp.segmented-button.outline.width` | `dimension` | `{tanrei.sys.border.hairline}` |
| `comp.segmented-button.divider.color` | `color` | `{tanrei.sys.color.outline}` |
| `comp.segmented-button.divider.width` | `dimension` | `{tanrei.sys.border.hairline}` |
| `comp.segmented-button.container.shape` | `dimension` | `{tanrei.sys.shape.corner.none}` |
| `comp.segmented-button.container.height.small` | `dimension` | `32px` |
| `comp.segmented-button.container.height.medium` | `dimension` | `36px` |
| `comp.segmented-button.container.height.large` | `dimension` | `40px` |
| `comp.segmented-button.container.padding-inline.small` | `dimension` | `{tanrei.sys.space.space150}` |
| `comp.segmented-button.container.padding-inline.medium` | `dimension` | `{tanrei.sys.space.space175}` |
| `comp.segmented-button.container.padding-inline.large` | `dimension` | `{tanrei.sys.space.space200}` |
| `comp.segmented-button.container.padding-block.small` | `dimension` | `6px` |
| `comp.segmented-button.container.padding-block.medium` | `dimension` | `6px` |
| `comp.segmented-button.container.padding-block.large` | `dimension` | `6px` |
| `comp.segmented-button.container.gap.small` | `dimension` | `{tanrei.sys.space.space100}` |
| `comp.segmented-button.container.gap.medium` | `dimension` | `{tanrei.sys.space.space100}` |
| `comp.segmented-button.container.gap.large` | `dimension` | `{tanrei.sys.space.space100}` |
| `comp.segmented-button.label.typescale.small` | `typography` | `{tanrei.sys.typescale.label}` |
| `comp.segmented-button.label.typescale.medium` | `typography` | `{tanrei.sys.typescale.label}` |
| `comp.segmented-button.label.typescale.large` | `typography` | `{tanrei.sys.typescale.label}` |
| `comp.segmented-button.icon.size.small` | `dimension` | `{tanrei.sys.icon.extra-small}` |
| `comp.segmented-button.icon.size.medium` | `dimension` | `{tanrei.sys.icon.extra-small}` |
| `comp.segmented-button.icon.size.large` | `dimension` | `{tanrei.sys.icon.extra-small}` |
| `comp.segmented-button.check.size.small` | `dimension` | `{tanrei.sys.icon.extra-small}` |
| `comp.segmented-button.check.size.medium` | `dimension` | `{tanrei.sys.icon.extra-small}` |
| `comp.segmented-button.check.size.large` | `dimension` | `{tanrei.sys.icon.extra-small}` |
| `comp.segmented-button.check.weight` | `number` | `{tanrei.sys.icon.mark.weight}` |
| `comp.segmented-button.unselected.container.color` | `color` | `{"colorSpace":"srgb","components":[0,0,0],"alpha":0,"hex":"#000000"}` |
| `comp.segmented-button.unselected.container.elevation` | `shadow` | `{tanrei.sys.elevation.level0}` |
| `comp.segmented-button.unselected.label.color` | `color` | `{tanrei.sys.color.on-surface}` |
| `comp.segmented-button.unselected.icon.color` | `color` | `{tanrei.sys.color.on-surface-variant}` |
| `comp.segmented-button.unselected.state-layer.color` | `color` | `{tanrei.comp.segmented-button.unselected.label.color}` |
| `comp.segmented-button.selected.container.color` | `color` | `{tanrei.sys.color.secondary-container}` |
| `comp.segmented-button.selected.container.elevation` | `shadow` | `{tanrei.sys.elevation.level0}` |
| `comp.segmented-button.selected.label.color` | `color` | `{tanrei.sys.color.on-secondary-container}` |
| `comp.segmented-button.selected.icon.color` | `color` | `{tanrei.sys.color.on-secondary-container}` |
| `comp.segmented-button.selected.state-layer.color` | `color` | `{tanrei.comp.segmented-button.selected.label.color}` |
| `comp.segmented-button.motion.state-duration.small` | `duration` | `{tanrei.sys.motion.role.state.compact}` |
| `comp.segmented-button.motion.state-duration.medium` | `duration` | `{tanrei.sys.motion.role.state.default}` |
| `comp.segmented-button.motion.state-duration.large` | `duration` | `{tanrei.sys.motion.role.state.large}` |
| `comp.segmented-button.motion.state-easing` | `cubicBezier` | `{tanrei.sys.motion.easing.state}` |
| `comp.segmented-button.motion.feedback-duration` | `duration` | `{tanrei.sys.motion.role.feedback}` |
| `comp.segmented-button.motion.selection-duration` | `duration` | `{tanrei.sys.motion.role.selection.mark}` |
| `comp.segmented-button.motion.selection-easing` | `cubicBezier` | `{tanrei.sys.motion.easing.state}` |

Per-state tokens, valued by the design system's pattern rules (not written by hand):

- `comp.segmented-button.<variant>.<state>.<property>` for the variants `unselected`, `selected`, the states `hovered`, `focused`, `pressed`, `disabled` and the properties `container.color`, `container.elevation`, `label.color`, `icon.color`, `state-layer.color`.
- `comp.segmented-button.<state>.<property>` for the states `hovered`, `focused`, `pressed`, `disabled` and the properties `opacity`, `container.shape`.

## Guidelines

### When to use

Use a segmented button when one small question has a few fixed answers and people should see them all at once: a chart’s range, a list’s layout, a paragraph’s alignment, the days a reminder repeats on. The effect should show right away, because with single selection the arrow keys choose as they move.

Two to five options fit. Past five, use a menu or a list. When the choice is the main task of a form, radio buttons read better. When the answer changes which content fills the screen, use [tabs](https://tanrei.chnotchy.com/components/tabs/).

### Segmented button, button group, chips or tabs

These four all put a few related things in a row, so they are easy to confuse. They differ in what a press does and in how the keyboard moves through them.

| Component | What it is | Selection | Keyboard | Use it for |
| --- | --- | --- | --- | --- |
| [Segmented button](https://tanrei.chnotchy.com/components/segmented-button/) | One control: two to five joined options that answer one question. | Its own, by value: one option, or several. | One Tab stop. The arrow keys move between options, and with single selection they also choose. | A setting with a few fixed answers that applies at once: Day, Week, Month; left, center, right. |
| [Button group](https://tanrei.chnotchy.com/components/button-group/) | A row or a column of separate buttons, spaced apart or joined into one shape. | None. A toggle button in the group keeps its own on or off state. | Every button is its own Tab stop. | Related actions: Cancel, Save draft, Publish; Bold, Italic and Underline as independent toggles. |
| [Chips](https://tanrei.chnotchy.com/components/chip/) | Separate small controls in a row that wraps or scrolls. | Each filter chip is on or off by itself. | Every chip is its own Tab stop. | Filters above a list, suggested replies, items the user entered. |
| [Tabs](https://tanrei.chnotchy.com/components/tabs/) | Views of one subject, each with its own panel. | One tab: the view on screen. | One Tab stop. The arrow keys move and select. | Switching which content is shown: Overview, Activity, Settings. |

A quick test: if pressing it does something, it is a button group. If it sets a value, it is a segmented button, or chips when the options are many or independent. If it swaps the content below, it is tabs.

### One choice or several

- **Single** (`selection="single"`, the default) is a radio group. Choosing a segment clears the one before it. Pressing the chosen segment again does nothing, so a single choice cannot be cleared by its own segment. The set may start with nothing chosen (`value` empty), for a form that must show that nothing has been picked yet.
- **Multiple** is a group of toggle buttons. Each segment switches on and off, and the set may end up with none. If your product needs at least one, don’t apply a change that would leave the set empty.

The set has no `disabled` of its own. Disable segments one by one. A disabled segment keeps its place and its outline, and the arrow keys skip it.

### Labels, icons and the check

- A selected segment shows a check before its label, in place of its icon when it has one, so the selection is never told by the fill alone. An icon-only segment keeps its icon and draws it filled. A design system may set the check’s size to 0: then it draws no check, and shows the selection with the fill and the elevation.
- Every segment is as wide as the widest one, measured as if it were selected, so the check appearing never moves a neighbour. Given less room, labels wrap onto more lines and every segment grows to the tallest. They never truncate.
- `labelHidden` draws the icon alone. The label is still required, because it stays the segment’s accessible name.
- The size sets the set’s height. The sizes are not Button’s heights at every step (in the `m3-like` preset small and medium match Button’s and large is lower), so match a set to the buttons beside it by height, not by the name of the size.

### Do

- Give the set a label that asks the question its options answer: “Range”, “Text alignment”.
- Keep labels to one or two words, and parallel in form: Day, Week, Month.
- Give every segment of a set an icon, or none.
- Choose a starting value when there is a sensible default, so the control never looks unanswered.

### Don’t

- Don’t use a segmented button for actions. A segment is a choice, and emits no click. Use a button group.
- Don’t use it to switch between views that each fill the screen. Use tabs.
- Don’t put more than five segments in a set, or labels of very different lengths. Every segment takes the widest one’s width.
- Don’t change a segment’s label with its state. The selection is announced separately.

## Accessibility

### What the component handles

- With single selection the set is a radio group named by its label, and each segment a radio with its checked state. With multiple selection the set is a group named by its label, and each segment a toggle button with its pressed state.
- Each segment is named by its own label, drawn or not, and exposes its position and the number of segments. The icons, the check, the dividers and the outline are hidden. The state is never announced from the check.
- The set is one Tab stop: the selected segment, or the one focused last with multiple selection, or else the first enabled one. The arrow keys move to the next or previous enabled segment and wrap, and Home and End reach the ends. With single selection each move also selects. With multiple selection Space and Enter switch the focused segment. In a right-to-left layout Left Arrow moves to the next segment.
- Disabled segments are skipped by the arrow keys, are never the Tab stop, and are exposed as disabled.
- Each segment’s hit area reaches above and below the set to the minimum target, and each segment is at least a target wide, because neighbours cannot reach over each other.
- The focus indicator is drawn inside the focused segment, where the outline and the neighbours cannot cover it. It shows for keyboard focus only.
- At larger text sizes labels wrap and every segment grows to the tallest. The icon keeps its size.
- Debug builds report a missing set or segment label, a hidden label without an icon, a value or values that name no segment, an empty or repeated segment value, and a set of fewer than two or more than five segments.

Known platform gaps:

- On the web, a multiple-choice set gives each segment its position through a list around the segments, because a toggle button cannot carry a position of its own.
- SwiftUI has no radio group role. The set is a container named by its label. A segment of a single-choice set is a button with the selected trait and its position (“2 of 3”) as its value, in English only for now. A segment of a multiple-choice set is a toggle and states no position, because a value of its own would replace the toggle’s on or off.
- SwiftUI: the set takes the width it is offered, as SwiftUI’s segmented picker does. `.fixedSize(horizontal: true, vertical: false)` keeps its natural width. A segment’s shadow and focus ring use its largest corner on every corner. An SF Symbol given as an icon does not switch to its filled form when selected: pass the `.fill` symbol yourself. The arrow keys are tested as rules, not with a keyboard on the simulator.
- Compose has no radio group role. The set is a selectable group, and each segment is a radio button (single) or a toggleable button (multiple), as the filter chip is.
- Flutter exposes neither a segment’s position nor the number of segments, and a multiple-choice set is a container named by its label rather than a group, because Flutter’s semantics have neither property.
- React Native: the equal widths come from measuring each label after the first layout, so a set whose parent is narrower than it on the first frame can take a width narrower than its widest label. The arrow keys on a hardware keyboard, and the raised selection’s shadow over its neighbours on Android, have not been checked on a device.
- Under the `cupertino` system, the small and medium sets are 30 and 34 tall rather than 28 and 32, and the inner corners of the end segments are rounder than their outer ones. Both wait for a token or spec decision.

### What you need to do

- Give the set a label, and every segment a label, including icon-only segments.
- Make the effect of a choice show at once, near the control. With single selection, a keyboard user chooses by moving, so a slow effect makes the arrow keys feel stuck. If it must take time, show progress where the result will appear.
- Give disabled segments a reason nearby when it is not obvious why they cannot be chosen.
- On the web the segmented button is not a form control. To post its value with a form, copy it into a hidden input from `change`.
