Skip to content

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.

Examples

Single choice

Use a connected button group in new code.

React HTML
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.

React HTML
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 check glyph) 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.