Actions
Segmented button
Deprecated (DECISIONS D4): a row of segments for single or multiple choice. Use a connected button group instead.
Deprecated (DECISIONS D4). This component ships until 2.0 for migration
and gets no new features; React warns once in development.
D4: use ButtonGroup with variant="connected" and
selectionMode="single" (m3.material.io no longer recommends segmented buttons). Shipped until
2.0; no new features.
- COMPONENT_SPEC §37
- CSS · React
- Open in the playground
Examples
Single choice
Use a connected button group in new code.
import { SegmentedButton, SingleChoiceSegmentedButtonRow } from '@kyralabs/m3exp-web/react';
/** Deprecated (DECISIONS D4): use a connected ButtonGroup. */
export default function Single() {
return (
<SingleChoiceSegmentedButtonRow aria-label="Range" defaultValue="day">
<SegmentedButton value="day">Day</SegmentedButton>
<SegmentedButton value="week">Week</SegmentedButton>
<SegmentedButton value="month">Month</SegmentedButton>
</SingleChoiceSegmentedButtonRow>
);
}
<div class="m3e-segmented-button-row" role="radiogroup" aria-label="Range">
<label class="m3e-segmented-button">
<input class="m3e-segmented-button__input" type="radio" name="range" value="day" checked />
<span class="m3e-segmented-button__icon" aria-hidden="true">
<span class="m3e-icon m3e-segmented-button__check" data-fill>check</span>
</span>
<span class="m3e-segmented-button__label">Day</span>
</label>
<label class="m3e-segmented-button">
<input class="m3e-segmented-button__input" type="radio" name="range" value="week" />
<span class="m3e-segmented-button__icon" aria-hidden="true">
<span class="m3e-icon m3e-segmented-button__check" data-fill>check</span>
</span>
<span class="m3e-segmented-button__label">Week</span>
</label>
<label class="m3e-segmented-button">
<input class="m3e-segmented-button__input" type="radio" name="range" value="month" />
<span class="m3e-segmented-button__icon" aria-hidden="true">
<span class="m3e-icon m3e-segmented-button__check" data-fill>check</span>
</span>
<span class="m3e-segmented-button__label">Month</span>
</label>
</div>
Run the HTML version (CSS classes only).
Multiple choice
Checkboxes, each its own Tab stop that Space toggles. Use a button group of toggle buttons in new code.
import { MultiChoiceSegmentedButtonRow, SegmentedButton } from '@kyralabs/m3exp-web/react';
/** Deprecated (DECISIONS D4): use a ButtonGroup of toggle buttons. */
export default function Multi() {
return (
<MultiChoiceSegmentedButtonRow aria-label="Filters" defaultValue={['walk', 'bike']}>
<SegmentedButton value="walk">Walk</SegmentedButton>
<SegmentedButton value="bike">Bike</SegmentedButton>
<SegmentedButton value="transit">Transit</SegmentedButton>
</MultiChoiceSegmentedButtonRow>
);
}
<div class="m3e-segmented-button-row" role="group" aria-label="Filters">
<label class="m3e-segmented-button">
<input
class="m3e-segmented-button__input"
type="checkbox"
name="filters"
value="walk"
checked
/>
<span class="m3e-segmented-button__icon" aria-hidden="true">
<span class="m3e-icon m3e-segmented-button__check" data-fill>check</span>
</span>
<span class="m3e-segmented-button__label">Walk</span>
</label>
<label class="m3e-segmented-button">
<input
class="m3e-segmented-button__input"
type="checkbox"
name="filters"
value="bike"
checked
/>
<span class="m3e-segmented-button__icon" aria-hidden="true">
<span class="m3e-icon m3e-segmented-button__check" data-fill>check</span>
</span>
<span class="m3e-segmented-button__label">Bike</span>
</label>
<label class="m3e-segmented-button">
<input class="m3e-segmented-button__input" type="checkbox" name="filters" value="transit" />
<span class="m3e-segmented-button__icon" aria-hidden="true">
<span class="m3e-icon m3e-segmented-button__check" data-fill>check</span>
</span>
<span class="m3e-segmented-button__label">Transit</span>
</label>
</div>
Run the HTML version (CSS classes only).
Usage
Compose SingleChoiceSegmentedButtonRow / MultiChoiceSegmentedButtonRow with SegmentedButton: two to five options, one or several selected.
API
React
<SingleChoiceSegmentedButtonRow>
Deprecated. D4: use ButtonGroup with variant="connected" and
selectionMode="single" (m3.material.io no longer recommends segmented buttons). Shipped until
2.0; no new features.
Single-choice segmented button row (COMPONENT_SPEC §37; Compose SingleChoiceSegmentedButtonRow): React Aria useRadioGroup on the m3e-css .m3e-segmented-button-row contract, a role="radiogroup" (Compose's selectableGroup) of <SegmentedButton>s on native radios. Arrow keys move the selection and wrap; Tab enters at the selected button. Name it with aria-label or aria-labelledby. The ref is the row element.
| Prop | Type | Default | Description |
|---|---|---|---|
value
|
string | null
|
Controlled selected value (null: none).
|
|
children
|
ReactNode
|
The <SegmentedButton>s.
|
|
name
|
string
|
The inputs' form name (generated when omitted). | |
defaultValue
|
string | null
|
||
isRequired
|
boolean
|
||
className
|
string
|
||
isDisabled
|
boolean
|
||
isReadOnly
|
boolean
|
||
form
|
string
|
||
id
|
string
|
||
aria-label
|
string
|
||
aria-labelledby
|
string
|
||
aria-describedby
|
string
|
||
onChange
|
(value: string) => void
|
Called with the newly selected value. | |
onFocus
|
(e: React.FocusEvent) => void
|
||
onBlur
|
(e: React.FocusEvent) => void
|
<MultiChoiceSegmentedButtonRow>
Deprecated. D4: use ButtonGroup with variant="connected" and
selectionMode="multiple" (m3.material.io no longer recommends segmented buttons). Shipped
until 2.0; no new features.
Multiple-choice segmented button row (COMPONENT_SPEC §37; Compose MultiChoiceSegmentedButtonRow): React Aria useCheckboxGroup on the m3e-css .m3e-segmented-button-row contract, a role="group" of <SegmentedButton>s on native checkboxes (each a Tab stop that Space toggles). Name it with aria-label or aria-labelledby. The ref is the row element.
| Prop | Type | Default | Description |
|---|---|---|---|
value
|
string[]
|
Controlled checked values. | |
children
|
ReactNode
|
The <SegmentedButton>s.
|
|
name
|
string
|
The inputs' form name (generated when omitted). | |
defaultValue
|
string[]
|
||
className
|
string
|
||
isDisabled
|
boolean
|
||
isReadOnly
|
boolean
|
||
form
|
string
|
||
id
|
string
|
||
aria-label
|
string
|
||
aria-labelledby
|
string
|
||
aria-describedby
|
string
|
||
onChange
|
(value: string[]) => void
|
Called with the checked values. | |
onFocus
|
(e: React.FocusEvent) => void
|
||
onBlur
|
(e: React.FocusEvent) => void
|
<SegmentedButton>
Deprecated. D4: use ButtonGroupItem in a connected ButtonGroup. Shipped until
2.0; no new features.
Segmented button (COMPONENT_SPEC §37; Compose SegmentedButton), inside a <SingleChoiceSegmentedButtonRow> (a native radio, React Aria useRadio) or a <MultiChoiceSegmentedButtonRow> (a native checkbox, useCheckboxGroupItem), on the m3e-css .m3e-segmented-button contract: the label around the input, the check slot and the label text. The check, the label slide and the item shapes are drawn by m3e-css; this drives the ripple. The ref is the native input.
| Prop | Type | Default | Description |
|---|---|---|---|
children
|
ReactNode
|
The label. | |
value
required
|
string
|
The value the row reports when this button is selected (checked). | |
icon
|
ReactNode
|
Icon shown while unselected, e.g. <Icon name="star" /> (Compose
SegmentedButtonDefaults.Icon(active, inactiveContent), SegmentedButtonMultiSelectSample): it
and the check cross-fade. Without it, the check scales in when the button is selected.
|
|
className
|
string
|
||
isDisabled
|
boolean
|
||
id
|
string
|
||
autoFocus
|
boolean
|
||
aria-label
|
string
|
||
aria-labelledby
|
string
|
||
aria-describedby
|
string
|
||
onPressStart
|
(e: PressEvent) => void
|
||
onPressEnd
|
(e: PressEvent) => void
|
||
onFocus
|
(e: React.FocusEvent) => void
|
||
onBlur
|
(e: React.FocusEvent) => void
|
CSS
segmented-button.css
@import '@kyralabs/m3e-css/segmented-button.css';
Segmented button (COMPONENT_SPEC §37) — @deprecated (DECISIONS D4): shipped until 2.0 for migration, no new features. Use the connected button group (.m3e-button-group--connected, <ButtonGroup variant="connected">) instead: m3.material.io no longer recommends segmented buttons since the M3 Expressive update (Compose itself does not deprecate them at the pin).
Native radios (single choice, Compose SingleChoiceSegmentedButtonRow) or checkboxes (multiple choice, MultiChoiceSegmentedButtonRow) inside labels: no JavaScript needed (owner decision M9 #29). Radios sharing a name select one at a time, arrow keys move the selection, Tab enters at the selected one, Space selects; checkboxes are Tab stops that Space toggles.
<div class="m3e-segmented-button-row" role="radiogroup" aria-label="Range">
<label class="m3e-segmented-button">
<input class="m3e-segmented-button__input" type="radio" name="range" value="day" checked />
<span class="m3e-segmented-button__icon" aria-hidden="true">
<span class="m3e-icon m3e-segmented-button__check" data-fill>check</span>
</span>
<span class="m3e-segmented-button__label">Day</span>
</label>
…
</div>
- Multiple choice: role="group" on the row and type="checkbox" inputs.
- An icon shown while unselected goes after the check in __icon (SegmentedButtonMultiSelectSample:
the check and it cross-fade). Without one, the check scales in when selected and the label
slides over the empty slot while unselected.
- Disabled: the input's
disabled.
33 custom properties it reads
| Custom property | Default |
|---|---|
--m3e-comp-minimum-interactive-component-size
|
48px |
--m3e-comp-outlined-segmented-button-container-height
|
40px |
--m3e-comp-outlined-segmented-button-icon-size
|
18px |
--m3e-comp-outlined-segmented-button-label-text-font
|
var(--m3e-sys-typescale-label-large-font) |
--m3e-comp-outlined-segmented-button-label-text-line-height
|
var(--m3e-sys-typescale-label-large-line-height) |
--m3e-comp-outlined-segmented-button-label-text-size
|
var(--m3e-sys-typescale-label-large-size) |
--m3e-comp-outlined-segmented-button-label-text-tracking
|
var(--m3e-sys-typescale-label-large-tracking) |
--m3e-comp-outlined-segmented-button-label-text-weight
|
var(--m3e-sys-typescale-label-large-weight) |
--m3e-comp-outlined-segmented-button-outline-width
|
1px |
--m3e-comp-outlined-segmented-button-shape
|
var(--m3e-sys-shape-corner-full) |
--m3e-comp-segmented-button-container-color-selected
|
var(--m3e-sys-color-secondary-container) |
--m3e-comp-segmented-button-container-color-selected-disabled
|
var(--m3e-sys-color-secondary-container) |
--m3e-comp-segmented-button-container-color-unselected
|
transparent |
--m3e-comp-segmented-button-content-color-disabled
|
color-mix(in srgb, var(--m3e-sys-color-on-surface) calc(0.38 * 100%), transparent) |
--m3e-comp-segmented-button-content-color-selected
|
var(--m3e-sys-color-on-secondary-container) |
--m3e-comp-segmented-button-content-color-unselected
|
var(--m3e-sys-color-on-surface) |
--m3e-comp-segmented-button-content-padding-horizontal
|
12px |
--m3e-comp-segmented-button-content-padding-vertical
|
8px |
--m3e-comp-segmented-button-icon-crossfade-duration
|
var(--m3e-sys-motion-spring-default-effects-duration) |
--m3e-comp-segmented-button-icon-crossfade-easing
|
var(--m3e-sys-motion-spring-default-effects-easing) |
--m3e-comp-segmented-button-icon-fade-duration
|
var(--m3e-sys-motion-spring-default-effects-duration) |
--m3e-comp-segmented-button-icon-fade-easing
|
var(--m3e-sys-motion-spring-default-effects-easing) |
--m3e-comp-segmented-button-icon-scale-duration
|
var(--m3e-sys-motion-spring-fast-spatial-duration) |
--m3e-comp-segmented-button-icon-scale-easing
|
var(--m3e-sys-motion-spring-fast-spatial-easing) |
--m3e-comp-segmented-button-icon-spacing
|
8px |
--m3e-comp-segmented-button-label-offset-duration
|
var(--m3e-sys-motion-spring-fast-spatial-duration) |
--m3e-comp-segmented-button-label-offset-easing
|
var(--m3e-sys-motion-spring-fast-spatial-easing) |
--m3e-comp-segmented-button-min-height
|
40px |
--m3e-comp-segmented-button-min-width
|
58px |
--m3e-comp-segmented-button-outline-color
|
var(--m3e-sys-color-outline) |
--m3e-comp-segmented-button-outline-color-disabled
|
color-mix(in srgb, var(--m3e-sys-color-outline) calc(0.12 * 100%), transparent) |
--m3e-comp-segmented-button-selected-z-index
|
5 |
--m3e-comp-segmented-button-state-layer-color
|
currentcolor |
Accessibility
single choice role="radiogroup" (Compose selectableGroup + Role.RadioButton), multiple choice role="group" of checkboxes (m3.material.io: "The role for the multi-select segmented button is Checkbox"); name the row with aria-label / aria-labelledby; each input is named by its label (the icon slot is aria-hidden).
Every interactive part has a target of at least 48 × 48 px, a visible focus ring for
keyboard focus only, and works under forced-colors and
prefers-reduced-motion.
Differences from Compose
the row sizes its equal columns from each button's whole label (CSS grid max-content), where Compose sizes them from the widest unbreakable word (IntrinsicSize.Min) and wraps longer labels; the icon and label mirror in RTL (Compose places them with place(), unmirrored) and so does the check's scale origin; Enter does not select (native inputs); springs are CSS linear() transitions (a retarget keeps no velocity) at fractional px (Compose's label offset is an Int Animatable); forced colors draw a 1dp border and fill the selected button with Highlight.
Sources
Values come from androidx Compose Material3 1.5.0-beta01 at 19991f2c and from
m3.material.io, as cited below from COMPONENT_SPEC §37 and the m3e-css sources.
SegmentedButton.kt, SegmentedButtonSamples.kt, Surface.kt, Button.kt (vendored), OutlinedSegmentedButtonTokens.kt; m3.material.io Segmented buttons (page 409b50be-403a-4846-932c-706a9b2137dd).
segmented-button.css sources
(compose/material3 @ 19991f2c, SegmentedButton.kt, Surface.kt, Button.kt; values in outlined-segmented-button.css / segmented-button-defaults.css, lines in PARITY.md):
- Rows (L327-380): Row(width(IntrinsicSize.Min), spacedBy(−BorderWidth), CenterVertically), each button weight(1f): equal widths from the widest button, neighbors overlapping by the 1dp border. Here an inline grid of 1fr columns sized from the buttons' max-content widths; each button reaches 1dp into the next column (a negative end margin) and the row pads its end by that 1dp, so every button is the widest one's width and the row is Compose's.
- Button (L130-251): a selectable (Role.RadioButton) or toggleable Surface: the 48dp minimum interactive size around it (Surface.kt L355-360, L490-495: the row is 48dp high, the 40dp button centered in it; the native input covers the target), defaultMinSize(ButtonDefaults MinWidth 58dp, MinHeight 40dp), the border drawn inside the shape and the ripple clipped to it (Surface.kt L544-564; an inset shadow here, the state layer over it).
- Shapes (itemShape L597-611): the first button gets the base shape's (full) start corners, the last its end corners, one alone all four, the others none.
- Colors (defaultSegmentedButtonColors L548-572): selected secondary container / on secondary container, unselected transparent / on surface, outline in both; disabled: content on surface at 0.38, the border Outline at 0.12, the selected container kept. Hover, focus and press change no color (state layer only, in the content color: ripple() without a color). Colors do not animate.
- Content (L383-457, SegmentedButtonDefaults.Icon L644-673): the 18dp icon slot, 8dp (IconSpacing), then the label in label large, centered in 12dp / 8dp padding. The slot keeps its width while empty and the label is drawn −(18 + 8) / 2 = −13dp back, so it looks centered, sliding on fast spatial (it overshoots under the expressive scheme). The check (Icons.Filled.Check, the Material Symbols
checkglyph) appears with a fade (default effects) and a scale from 0 about its bottom corner on the slot's start side (fast spatial), and disappears at once (ExitTransition.None). With an unselected icon the two cross-fade (default effects). - Raised (interactionZIndex L843-852): a selected button (CheckedZIndexFactor 5), plus one for focus and one for a press, so its border and focus ring draw over its neighbors.