A segmented button is a small set of related options shown side by side inside one outlined pill — a view switcher, a date range, a filter over a chart. Two to five segments; beyond that use a select or chips.
The root is a <fieldset class="segmented-button">
with a <legend>. Each segment is an
<input> and the
<label class="segment"> beside it, tied together
by id and for. That is the whole component:
there is no plugin, nothing to initialize, and no selected class to
keep in sync. The input holds the state and the form value, and the
browser moves it.
The control is the label's sibling rather than its child, the same
way a filter chip is written.
A <label> wrapping a radio or a checkbox
is one as far as the rest of the framework is concerned, and
would be painted as one — a 20dp ring on the control and a 48dp row
around it.
The legend names the group for assistive technology and is not shown — Material 3's anatomy has no visible group label, but a set of radios is only a set because a fieldset says so, and a fieldset is only named by its legend.
There is no modifier that shows it, and unhiding it by hand is not
one override but six. The root is both the fieldset and the grid of
segments, so a legend back in flow becomes another column beside
them. When the group needs a visible label, write one before the
fieldset — a heading or a <p> — and leave the
legend saying the same thing as the group's name.
Single select
Radios sharing a name. Exactly one segment is chosen,
the arrow keys move between them and Tab enters and leaves the group
as one stop — all of it the browser's, because the control is a real
radio group. Mark the initial choice with checked.
<fieldset class="segmented-button">
<legend>View</legend>
<input type="radio" id="view-day" name="view" value="day" checked>
<label class="segment" for="view-day">Day</label>
<input type="radio" id="view-week" name="view" value="week">
<label class="segment" for="view-week">Week</label>
<input type="radio" id="view-month" name="view" value="month">
<label class="segment" for="view-month">Month</label>
</fieldset>
Multi select
Checkboxes instead. Any number of segments can be on at once, each one its own Tab stop and its own form value. Nothing else changes — the input type is the entire difference between the two variants.
<fieldset class="segmented-button">
<legend>Filter transport</legend>
<input type="checkbox" id="transport-walk" name="transport" value="walk" checked>
<label class="segment" for="transport-walk">Walk</label>
<input type="checkbox" id="transport-transit" name="transport" value="transit" checked>
<label class="segment" for="transport-transit">Transit</label>
<input type="checkbox" id="transport-bike" name="transport" value="bike">
<label class="segment" for="transport-bike">Bike</label>
</fieldset>
With icons
Put a <span class="material-symbols"> in the
label, before or after the text, at 18dp. The segment's text is its
accessible name, so the icon is decoration and is
aria-hidden. Material 3 shows a check on the chosen
segment; write that icon yourself if you want it — nothing swaps it
in, and the filled container already says which one is on.
<fieldset class="segmented-button">
<legend>Map layer</legend>
<input type="radio" id="layer-map" name="layer" value="map" checked>
<label class="segment" for="layer-map">
<span class="material-symbols" aria-hidden="true">map</span>
Map
</label>
<input type="radio" id="layer-satellite" name="layer" value="satellite">
<label class="segment" for="layer-satellite">
<span class="material-symbols" aria-hidden="true">satellite_alt</span>
Satellite
</label>
</fieldset>
Disabled
disabled on one input greys that segment; on the
fieldset it greys the whole group, outline included, the way any
fieldset disables what it contains.
<fieldset class="segmented-button" disabled>
<legend>Sort</legend>
<input type="radio" id="sort-newest" name="sort" value="newest" checked>
<label class="segment" for="sort-newest">Newest</label>
<input type="radio" id="sort-oldest" name="sort" value="oldest">
<label class="segment" for="sort-oldest">Oldest</label>
</fieldset>
Semantics
A
composite role
such as radiogroup promises arrow-key navigation, and
this component rejects it rather than withholding it: a
fieldset of radios already is one, with the keyboard model
implemented by the browser. Writing the role by hand would restate
what the element already says and take away the group role the
fieldset gives you. No composite role is accepted on the group at
all.
What that costs is that the markup has to stay native, so the docs
suite enforces it — the root must be a <fieldset>,
every segment a <label for> against a real
<input>, and every radio must carry the shared
name that makes it a group, since that is where the
arrow keys come from. aria-checked and its neighbours
are refused: the input's checked state is the answer,
and a hand-written copy is a second one that nothing updates.
Tokens
Set these on the group itself, or in a rule of your own that picks
out the groups you want to change —
.chart-controls .segmented-button { … }. They are
declared on the component, so a value set on an ancestor
element is shadowed by the group's own default rather than
inherited.
The group is a grid of equal columns filling the width it is given, which is what Material specifies — constrain it with a width or a wrapper when it should be narrower than its container.
| Token | Default |
|---|---|
--md-comp-outlined-segmented-button-container-height | 40px |
--md-comp-outlined-segmented-button-container-shape | 9999px |
--md-comp-outlined-segmented-button-outline-width | 1px |
--md-comp-outlined-segmented-button-outline-color | var(--md-sys-color-outline) |
--md-comp-outlined-segmented-button-leading-space | 12px |
--md-comp-outlined-segmented-button-trailing-space | 12px |
--md-comp-outlined-segmented-button-icon-size | 18px |
--md-comp-outlined-segmented-button-icon-label-space | 8px |
--md-comp-outlined-segmented-button-label-text-color | var(--md-sys-color-on-surface) |
--md-comp-outlined-segmented-button-selected-container-color | var(--md-sys-color-secondary-container) |
--md-comp-outlined-segmented-button-selected-label-text-color | var(--md-sys-color-on-secondary-container) |
The state layer is the label colour mixed into the container at the state layer opacity, so those two colour tokens carry hover and press with them.