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#

TSX
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#

TSX
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.

TSX
<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#

TSX
<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#

TSX
<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#

TSX
<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#

TSX
<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#

PartElementStatesFlagsNotes
rootdiv—disabled, invalid, requiredrole="group", aria-label from label (else aria-labelledby to a Field's label), data-orientation. Carries the variant axes.
itembuttonon | offdisabled, selected, focus-visible, pressed, press-animatingtype="button", aria-pressed, data-orientation, roving tabIndex. Inside root. asChild.
hidden-inputselect——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#

PropTypeDefaultDescription
modelstring[]—Two-way binding of the pressed values, in both selection modes.
defaultValuestring[][]Initial value when uncontrolled.
valueChangeevent (value: string[])—Fires whenever the selection changes.
multiplebooleanfalseAllow more than one item on at a time; a press appends or removes instead of replacing.
deselectablebooleantrueIn single mode, pressing the on item turns it off.
loopbooleantrueArrow keys wrap from the last item to the first.
labelstring—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.
disabledbooleanfalseDisables every item; renders data-disabled on the root. A disabled group never posts.
namestring—Form field name; renders the hidden-input part.
formstring—Associates the hidden select with a <form> by id.
requiredbooleanfalseA platform constraint on the hidden select; renders data-required.
invalidbooleanfalseRenders data-invalid.
color / size / variant / axes / modsdesign-system vocabulary—The variant axes, rendered as data-* on root.
classstring—Extra classes on the root element.
aria-* / data-* / id / titleHTML 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#

PropTypeDefaultDescription
valuestringrequiredThe value this item toggles in the model.
disabledbooleanfalseSkipped by roving focus; renders data-disabled (and aria-disabled under asChild).
asChildbooleanfalseRender through the default slot, which receives the part bag.
classstring—Extra classes.

Keyboard#

KeyAction
ArrowRight / ArrowLeft (horizontal), ArrowDown / ArrowUp (vertical)Move focus to the next / previous enabled item without changing the selection. Mirrored under RTL.
Home / EndFirst / last enabled item.
Enter / Space / clickToggle the focused item.
TabLeaves 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.

Toggle for one standalone two-state button · RadioGroup for a form-participating exclusive choice · Tabs when the selection switches a panel.