SegmentedButtonGroup
import { useState } from 'react'
import { Icon, SegmentedButtonGroup, Surface, Text } from '@language-lit/material3-expressive'
const viewSegments = [
{ value: 'day', label: 'Day' },
{ value: 'week', label: 'Week' },
{ value: 'month', label: 'Month' },
]
const alignSegments = [
{ value: 'left', label: 'Left', icon: <Icon source="format_align_left" /> },
{ value: 'center', label: 'Center', icon: <Icon source="format_align_center" /> },
{ value: 'right', label: 'Right', icon: <Icon source="format_align_right" /> },
]
const filterSegments = [
{ value: 'walk', label: 'Walk' },
{ value: 'bike', label: 'Bike' },
{ value: 'transit', label: 'Transit' },
{ value: 'drive', label: 'Drive' },
]
const notificationSegments = [
{ value: 'email', label: 'Email', icon: <Icon source="mail" /> },
{ value: 'sms', label: 'SMS', icon: <Icon source="sms" /> },
{ value: 'push', label: 'Push', icon: <Icon source="notifications" /> },
]
export function SegmentedButtonGroupExample() {
const [view, setView] = useState('day')
const [align, setAlign] = useState('left')
const [filters, setFilters] = useState<readonly string[]>(['walk'])
const [notifications, setNotifications] = useState<readonly string[]>(['email', 'push'])
return (
<Surface
as="section"
aria-labelledby="segmented-button-group-example-title"
color="surface-container-low"
shape="extra-large"
className="segmented-button-group-example"
>
<Text
as="h2"
id="segmented-button-group-example-title"
variant="titleLarge"
emphasis="emphasized"
>
Segmented button groups
</Text>
<Text as="p" variant="bodyMedium">
Single- and multi-choice groups built on native radio and checkbox
inputs, with grouped corner shaping and an icon-to-checkmark
crossfade.
</Text>
<div className="segmented-button-group-example__row">
<SegmentedButtonGroup
segments={viewSegments}
aria-label="View"
value={view}
onValueChange={setView}
/>
<SegmentedButtonGroup
segments={alignSegments}
aria-label="Alignment"
value={align}
onValueChange={setAlign}
/>
</div>
<div className="segmented-button-group-example__row">
<SegmentedButtonGroup
multiple
segments={filterSegments}
aria-label="Travel modes"
value={filters}
onValueChange={setFilters}
/>
</div>
<div className="segmented-button-group-example__row">
<SegmentedButtonGroup
segments={viewSegments}
aria-label="Unavailable view"
defaultValue="week"
disabled
/>
<SegmentedButtonGroup
segments={[
{ value: 'day', label: 'Day' },
{ value: 'week', label: 'Week', disabled: true },
{ value: 'month', label: 'Month' },
]}
aria-label="Partly unavailable view"
defaultValue="day"
/>
</div>
<div className="segmented-button-group-example__row">
<SegmentedButtonGroup
multiple
segments={notificationSegments}
aria-label="Notification channels"
value={notifications}
onValueChange={setNotifications}
/>
<SegmentedButtonGroup
multiple
segments={filterSegments}
aria-label="Unavailable travel modes"
defaultValue={['walk']}
disabled
/>
</div>
</Surface>
)
}SegmentedButtonGroup renders a row of Material segmented buttons from one
declarative segments array — a native radio group for mutually exclusive
selection, or independent native checkboxes for multi-selection. There is no
compound-children API: content, order, and count all come from segments.
import { SegmentedButtonGroup } from '@language-lit/material3-expressive'
import '@language-lit/material3-expressive/styles.css'
<SegmentedButtonGroup
segments={[
{ value: 'day', label: 'Day' },
{ value: 'week', label: 'Week' },
{ value: 'month', label: 'Month' },
]}
aria-label="View"
value={view}
onValueChange={setView}
/>Contract
- Each segment renders one native
<input type="radio">(single-choice, the default) or<input type="checkbox">(multiple: true) wrapped in its own native<label>— the label's visible text is that control's accessible name, with no separateidplumbing. - The group root carries
role="radiogroup"orrole="group"; its accessible name comes from a caller-suppliedaria-label/aria-labelledby. value/defaultValue/onValueChangearestringin single-choice mode andreadonly string[]in multi-choice mode, discriminated by themultipleliteral so the two shapes cannot be mixed at the type level.namedefaults to a generated id shared by every segment's control; supply your own to control the submitted form field name.disabledat the group level disables every segment; a segment's owndisableddisables only that one.classNameandstyledescribe the group root; the ref forwards to that same root element.
Selection mode
multiple | Control | Selection |
|---|---|---|
false (default) | <input type="radio">, shared name | Mutually exclusive; native roving-tabindex keyboard behavior with no custom key handling |
true | <input type="checkbox">, shared name | Independent; every control reaches sequential focus |
Because selection is entirely native, an uncontrolled single-choice group's mutual exclusivity is browser-owned even without this component re-rendering — the same guarantee this library's Radio already relies on. A native form reset restores every control's own default selection with no library-owned state involved in that restoration.
<SegmentedButtonGroup
multiple
segments={travelModes}
aria-label="Travel modes"
value={filters}
onValueChange={setFilters}
/>Single-choice submits one value under its shared name; multi-choice's
checked values are all retrievable through FormData.getAll(name).
Shape
The first segment rounds only its inline-start corners, the last rounds only its inline-end corners, interior segments stay square, and a lone segment is fully rounded — mirroring correctly under RTL since the rounding follows logical corner properties, not physical sides. Adjacent segments overlap by exactly one border width so shared edges coincide instead of doubling, and a selected or currently-interacting segment's border stays visually on top of its neighbors'.
Icons
<SegmentedButtonGroup
segments={[
{ value: 'left', label: 'Left', icon: <Icon source="format_align_left" /> },
{ value: 'center', label: 'Center', icon: <Icon source="format_align_center" /> },
]}
aria-label="Alignment"
value={align}
onValueChange={setAlign}
/>A segment without a supplied icon shows only a built-in checkmark, which
fades and scales in on selection and disappears instantly on deselection. A
segment with a supplied icon shows that icon while unselected and
crossfades to the checkmark while selected, in both directions.
Tokens and boundaries
All color, geometry, and motion values live in one
--m3e-comp-segmented-button-group-* registration. Theme overrides remain
scoped to Material3Provider; SegmentedButtonGroup injects no runtime
styles. It imports no Next.js, Vite, router, animation
library, or private application code.