Toggle Group
A set of two-state buttons under one value model — text alignment, a view
switcher, a filter chip row. The root is a role="group" and every item a native <button>
with aria-pressed. The model is always string[]: multiple changes how a click writes it,
never its shape, so switching a group from single to multiple selection is not a type
migration.
Import
import { ToggleGroup } from '@sigx/zero/toggle-group';
ToggleGroup is a compound: ToggleGroup.Root, ToggleGroup.Item. It is also re-exported
from the @sigx/zero root, together with toggleGroupAnatomy and useToggleGroupContext.
Usage
import { component } from 'sigx';
import { ToggleGroup } from '@sigx/zero/toggle-group';
const Alignment = component(({ signal }) => {
const state = signal({ align: ['left'] });
return () => (
<ToggleGroup.Root model={() => state.align} label="Text alignment">
<ToggleGroup.Item value="left">Left</ToggleGroup.Item>
<ToggleGroup.Item value="center">Center</ToggleGroup.Item>
<ToggleGroup.Item value="right">Right</ToggleGroup.Item>
<ToggleGroup.Item value="justify" disabled>Justify</ToggleGroup.Item>
</ToggleGroup.Root>
);
});
model={() => state.align} binds the array of pressed values both ways. Leave the model off
and pass defaultValue to keep the state inside the component; valueChange fires either way.
See Models. label names the group (aria-label) — a row of
"Left / Center / Right" buttons means nothing to a screen reader without it.
Single selection
Without multiple at most one value is in the array: pressing an item replaces the array with
[value], and pressing the item that is already on empties it. Pass deselectable={false} to
keep one item always on, the way a radio row behaves.
<ToggleGroup.Root defaultValue={['grid']} deselectable={false} label="View">
<ToggleGroup.Item value="grid">Grid</ToggleGroup.Item>
<ToggleGroup.Item value="list">List</ToggleGroup.Item>
</ToggleGroup.Root>
Multiple selection
<ToggleGroup.Root model={() => state.styles} multiple label="Text style">
<ToggleGroup.Item value="bold">B</ToggleGroup.Item>
<ToggleGroup.Item value="italic">I</ToggleGroup.Item>
<ToggleGroup.Item value="underline">U</ToggleGroup.Item>
</ToggleGroup.Root>
Under multiple a press appends the value or filters it out; every item is independent.
Vertical groups
<ToggleGroup.Root defaultValue={[]} orientation="vertical" label="Sidebar">
orientation renders as data-orientation on the root and on every item — the divider
between items is directional CSS on the item, and a sibling selector cannot see the root — and
switches the roving keys.
In a form
<form action="/issues">
<ToggleGroup.Root name="status" multiple defaultValue={['open']} required label="Status">
<ToggleGroup.Item value="open">Open</ToggleGroup.Item>
<ToggleGroup.Item value="closed">Closed</ToggleGroup.Item>
</ToggleGroup.Root>
</form>
ToggleGroup takes the form contract — name, form, disabled, invalid, required — and
posts the way Select does. The form above submits status=open, and
status=open&status=closed with both pressed. With a name it renders the hidden-input
part, a real, visually-hidden <select> holding the pressed values, so a segmented control
posts before hydration: one field in single mode, a repeated field per pressed value under
multiple. required is a platform constraint — submitting with nothing pressed fails
validation, and the invalid focus lands on the group's tab stop. A disabled group never posts,
form="id" associates it with a form outside its subtree, and the owning form's reset()
restores defaultValue.
Inside a Field.Root the group adopts the Field's
disabled, invalid and required flags and its size, and is named by the Field's label
(aria-labelledby) unless label is given.
Rendering an item as your own element
<ToggleGroup.Item value="docs" asChild>
{(p) => <span {...p}>Docs</span>}
</ToggleGroup.Item>
With asChild the default slot receives the part's attribute bag; spread it so the anatomy,
aria-pressed, the roving tabIndex and the handlers land on your element. The button
contract is supplied by hand — role="button", aria-disabled when disabled, and Enter /
Space activation where the platform does not synthesise a click.
Anatomy
| Part | Element | States | Flags | Notes |
|---|---|---|---|---|
root | div | — | disabled, invalid, required | role="group", aria-label from label (else aria-labelledby to a Field's label), data-orientation. Carries the variant axes. |
item | button | on | off | disabled, selected, focus-visible, pressed, press-animating | type="button", aria-pressed, data-orientation, roving tabIndex. Inside root. asChild. |
hidden-input | select | — | — | The posted value: a real, visually-hidden <select>, multiple under multiple. Rendered only while name is set. Inside root. |
Every part carries data-scope="toggle-group". An item mirrors the standalone
Toggle's on / off contract so a design system can share
fill styles between the two, and doubles the on-state as a data-selected presence flag —
[data-part="item"][data-selected] composes with other flags where a data-state match
cannot. disabled on the root disables every item; each item renders its own data-disabled.
Items publish press feedback. See
The anatomy contract.
Props
ToggleGroup.Root
| Prop | Type | Default | Description |
|---|---|---|---|
model | string[] | — | Two-way binding of the pressed values, in both selection modes. |
defaultValue | string[] | [] | Initial value when uncontrolled. |
valueChange | event (value: string[]) | — | Fires whenever the selection changes. |
multiple | boolean | false | Allow more than one item on at a time; a press appends or removes instead of replacing. |
deselectable | boolean | true | In single mode, pressing the on item turns it off. |
loop | boolean | true | Arrow keys wrap from the last item to the first. |
label | string | — | Accessible name of the role="group" container, rendered as aria-label. |
orientation | 'horizontal' | 'vertical' | 'horizontal' | Roving-key axis; rendered as data-orientation on root and items. |
disabled | boolean | false | Disables every item; renders data-disabled on the root. A disabled group never posts. |
name | string | — | Form field name; renders the hidden-input part. |
form | string | — | Associates the hidden select with a <form> by id. |
required | boolean | false | A platform constraint on the hidden select; renders data-required. |
invalid | boolean | false | Renders data-invalid. |
color / size / variant / axes / mods | design-system vocabulary | — | The variant axes, rendered as data-* on root. |
class | string | — | Extra classes on the root element. |
aria-* / data-* / id / title | HTML attributes | — | Forwarded to the root; role is refused (the root is the group). An app aria-labelledby / aria-describedby joins the component's. |
ToggleGroup.Item
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | required | The value this item toggles in the model. |
disabled | boolean | false | Skipped by roving focus; renders data-disabled (and aria-disabled under asChild). |
asChild | boolean | false | Render through the default slot, which receives the part bag. |
class | string | — | Extra classes. |
Keyboard
| Key | Action |
|---|---|
| ArrowRight / ArrowLeft (horizontal), ArrowDown / ArrowUp (vertical) | Move focus to the next / previous enabled item without changing the selection. Mirrored under RTL. |
| Home / End | First / last enabled item. |
| Enter / Space / click | Toggle the focused item. |
| Tab | Leaves the group. |
Roving focus is pure navigation here — a toggle activates on click, Space or Enter, never on focus. One item is in the tab order: the first enabled item that is on, else the first enabled item. See Behaviors.
In the shipped design systems
Both @sigx/zero-basic and @sigx/zero-daisyui wire color (all eight recommended roles,
defaulting to primary) and size (xs–xl) on toggle-group, so
<ToggleGroup.Root color="secondary" size="lg"> is styled in both. Neither wires a variant
or any mods on the scope; under a design system's /register import those props are
therefore absent. See Typed vocabulary.
Related
Toggle for one standalone two-state button ·
RadioGroup for a form-participating exclusive choice ·
Tabs when the selection switches a panel.
