flyingsalmon

Toggle Group

Chips that toggle, in single or multiple mode, wrapping across rows. A member of the field family: it owns its label and its error message, takes required, and posts through name. Two sizes, one per-item color variant.

className styles the wrapper

ToggleGroup renders a wrapper around the chip row so it can hold the group label and the error message, the same as Radio Group and Select. className styles that wrapper, not the row. On ToggleGroupItem it styles the chip. Every other prop passes through to the matching Radix part.

Single

mode defaults to ToggleGroupMode.Single: value is a string and onValueChange hands back a string. Clicking the pressed chip deselects it and reports an empty string. That return to empty is the reason toggle-group sits beside Radio Group rather than inside it — a radio group cannot be emptied by the user. Each item takes an optional 16px leading icon on Button's slot rule.

Light
Activity type

Filtering by food.

Dark
Activity type

Filtering by food.

Multiple and max

ToggleGroupMode.Multiple switches value to an array and adds max. At the cap every unpressed chip takes the disabled look and stops toggling, while pressed chips stay live so the user can always back out of a pick. The registry renders no counter and no hint: the label says the limit. A capped chip and a disabled chip are deliberately indistinguishable — both are unavailable right now, and the label already explains why.

Light
Pick up to 3
Dark
Pick up to 3

Required

required means the group must hold a value. The component blocks the deselect that would empty it, in both modes, and the field label renders its marker. This is the field family's meaning of required. It is not a submit-time check: a group mounted empty is reachable, and a submit while it is still empty is the app's validation error, shown through error.

Light
Trip pace
Trip pace

Pick a pace before generating

Dark
Trip pace
Trip pace

Pick a pace before generating

Amber

variant on an item takes ToggleGroupItemVariant.Amber, which swaps the pressed fill from --primary to amber-400 with neutral-950 text, Button's amber row. Only the pressed fill changes: an unpressed amber chip is the same grey as every other chip, and it toggles like every other one. It marks an option that needs a step the label names, such as a visa or a permit. A chip that fires an action and never toggles is the app's to compose from a Button; the registry ships no such rule.

Light
Destinations
Dark
Destinations

Sizes and states

size sits on the group, not on the item: ToggleGroupSize.Default is 36px and ToggleGroupSize.Small is 32px, matching Input and Button. Chips are fully rounded and wrap across rows. A chip rests on --secondary with its border in the same color, so hover shows up as the border stepping to --accent; pressed fills --primary and steps one shade lighter on hover. Feedback lives in the border and the fill, never in elevation. A held press draws Button's tight 2px ring with no scale and no translate (ADR 0003).

Light
Default, 36px
Small, 32px
One item disabled
Whole group disabled
Dark
Default, 36px
Small, 32px
One item disabled
Whole group disabled

Not a segmented control, not a badge

Tabs draw a shared track and switch the panel underneath; a segmented control is that same shape borrowed for a value. Toggle Group has no track, no fixed row, and no panel: the chips wrap, multiple mode exists, and single mode returns to empty. If the choice navigates, it is Tabs. If it answers a question, it is Toggle Group. And a chip the user cannot press is a Badge — a read-only summary of activity types, or the chosen values echoed back on a review screen, is Badge and not a disabled group. There is no separate chip primitive.

Forms

name puts the raw values into the surrounding form's FormData through hidden inputs: one in single mode, one per pressed chip in multiple mode, and none at all while the group is empty. value with onValueChange hands the selection to the app and defaultValue leaves it with the component — the props a form-state library drives, and the registry binds to none of them itself (ADR 0007).

Light
Interests

Posting 1 hidden input named interests.

Dark
Interests

Posting 1 hidden input named interests.

Motion

The fill and the border swap on a CSS transition at --motion-fast, and that is the whole of it. Toggle Group has no motion dependency — chips added while the group is mounted appear with no enter animation, on Badge's precedent, and an app that mounts options dynamically wraps them itself. The animated error message below the row comes from the field item.

Accessibility

Single mode exposes role="radiogroup" with role="radio" chips carrying aria-checked; multiple mode exposes a group of buttons carrying aria-pressed. The group is named by its label through aria-labelledby, takes aria-required in single mode, and an error sets aria-invalid and links the message through aria-describedby. The row is one tab stop. Tab lands on the pressed chip, Left and Right move and wrap at the ends, Home and End reach them directly, Space and Enter toggle, and Up and Down do nothing — a wrapped row has no vertical order to follow. Disabled and capped chips leave the arrow order. The focus ring is Button's keyboard-only offset ring.