Skip to content
Folio

Forms

ToggleGroup

A segmented control: several options, one strip.

Usage

When to reach for it

It changes a VALUE. Something that switches panels is Tabs.
TSX
import { ToggleGroup } from '@misoto22/design'

Notes

A segmented control: several options, one strip.

type="single" is a choice — Radix gives it radio semantics, and it is the right shape for a view switcher or a density setting. type="multiple" is a set of independent toggles, which is a different thing announced differently; choosing the wrong one is how a "filter by tag" control ends up telling a screen reader that picking one tag unpicks the others.

The two look different on purpose. A single-value strip moves ONE filled pill between its options, so the eye follows a thing travelling; a multiple-value strip fills each pressed option separately, because there is no single selection to travel. Two people looking at a screenshot should be able to tell which kind they are looking at, and before this they could not.

Distinct from Tabs, which switches PANELS and owns a tabpanel relationship. A toggle group changes a value.

Inside a Field the strip takes its name from that label, by pointing back at it — the root is a div, and <label for> does not bind to one, so the words above it do not click through. Standing alone it still needs its own aria-label.

Anatomy

ToggleGroup anatomy
ElementDescription
StriprequiredThe bordered pill holding the segments, w-fit as well as inline-flex — without it a flex or grid parent stretches the strip to the widest sibling and leaves dead space after the last segment.
Travelling pillThe --accent block behind the selection, on single-value groups only and only once it has measured a selected segment. It moves rather than cross-fading, so the eye follows one thing.
SegmentrequiredA button at --control-h-sm. In a single group it changes ink only and lets the pill behind it do the filling; in a multiple group it fills itself, because there is nothing travelling.
Segment contentchildren, in a gap-2 row. There is no iconOnly path here the way Button has one, so an icon with no words is a segment with no name.

Best practices

Do

  • Give a single-value group a defaultValue or a value. The pill appears only after it has measured a selected segment, so a group that starts empty is a bare strip with nothing marked in it.
  • Name the strip. Inside a Field its label does it, through aria-labelledby — the root is a div, so there is nothing for htmlFor to bind to — and standing alone it needs its own aria-label, which with type="single" is what a radiogroup is announced by.
  • Give an icon-only segment its own aria-label: nothing strips the text or supplies a name for you here, so a strip of three glyphs announces three unnamed buttons.

Don’t

  • type="single" has radio semantics but not radio behaviour: pressing the selected segment deselects it and commits an empty string, so a view switcher built on it can be switched off into no view at all.
  • Segments are --control-h-sm — 36px comfortable, 30px under data-density="compact" — which is below the 44px pointer floor (WCAG 2.5.5). A strip meant for a thumb needs its own height.
  • Do not put six options in it. The strip neither wraps nor scrolls, so past about five segments it simply runs out past its container, and that is a Select or a Combobox anyway.
  • A Field’s required marks a single-value strip and leaves a multiple-value one unmarked: that one is a role="toolbar", which takes no aria-required at all, so on type="multiple" the asterisk is the whole of the marking.

Examples

default

The same strip in both of its modes. single has radio semantics and moves one filled pill between its options, so the eye follows a thing travelling; multiple fills each pressed segment on its own, because there is no single selection to travel. The choice is not cosmetic — the wrong one tells a screen reader that picking one format unpicks the others. A single group needs a defaultValue or a value: the pill appears only once it has measured a selected segment, so a group that starts empty is a bare strip with nothing marked in it. Neither wraps nor scrolls, so stop at about five segments.

single — one of these

multiple — any of these

icon only

Four segments carrying a glyph and no word, for a toolbar where the word would only repeat what the row already says. There is no iconOnly path here the way Button has one — nothing strips the text or supplies a name for you — so each segment needs its own aria-label, and a strip without them announces four unnamed buttons. Mind the height as well: segments are --control-h-sm, 36px comfortable and 30px under compact, which is under the 44px pointer floor, so a strip meant for a thumb needs a height of its own.

never switched off

type="single" has radio semantics but not radio behaviour: pressing the selected segment deselects it and commits an empty string, so a view switcher built on it can be switched off into no view at all. Controlled, the guard is one line — ignore the empty string and the strip keeps the value it had. Press the selected segment and watch it stay.

Showing the grid view.

Parts

Composed at the call site rather than configured through props, so a layout this component did not anticipate is still expressible.

ToggleGroupItem

One segment.

In a single-value group the item draws no background of its own — the travelling pill behind it does — so it only changes ink. In a multiple-value group it fills, because there is nothing travelling.

Also accepts everything in ComponentProps<typeof ToggleGroupPrimitive.Item>. Those are forwarded to the underlying element and are not listed row by row.

Keyboard

ToggleGroup keyboard interactions
KeyDoes
TabMoves into the strip — one stop for the group.
Moves between segments.
EnterSpaceToggles the focused segment.

Accessibility

  • type="single" gets radio semantics; type="multiple" gets independent toggles. Choosing wrong tells a screen reader that picking one option unpicks the others.