Toggle Group
A set of two-state buttons sharing one selection — single or multiple, three sizes, adjustable spacing, and a vertical axis.
- Status
- Since
0.1.0- Accessibility pattern
- role=group over native toggle buttons
Last updated
Install
Configure registry credentials first using the Quickstart. Before copying this item or any transitive registry dependency, verify the signed manifest and save the exact bytes for Toggle Group before installation. Retain the digest printed by this command.
pnpm exec vegastack-design verify toggle-groupInstall with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.
pnpm dlx shadcn@4.21.0 add @vegastack/toggle-groupRun the exact offline --post-write command printed by the preflight, using its saved item path and independently retained --expected-integrity digest. Use the saved preflight for every transitive registry dependency too; stop on any mismatch before using the components.
The same command installs the registry items it composes: @vegastack/toggle.
Usage
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
<ToggleGroup defaultValue={["a"]}>
<ToggleGroupItem value="a">A</ToggleGroupItem>
<ToggleGroupItem value="b">B</ToggleGroupItem>
<ToggleGroupItem value="c">C</ToggleGroupItem>
</ToggleGroup>;Anatomy
ToggleGroup is a compound component. Compose the items inside the root:
ToggleGroup
├── ToggleGroupItem
└── ToggleGroupItemToggleGroup— therole="group"container (data-slot="toggle-group"). Owns the selection (value,defaultValue,onValueChange,multiple), the sharedvariantandsizeit passes to every item through context, and thespacingandorientationthat shape the row.ToggleGroupItem— one native toggle button (data-slot="toggle-group-item"), identified by itsvalueand styled bytoggleVariantsfrom thetoggleitem. It readsvariant,sizeandspacingfrom the group rather than taking them per item.
Examples
Composition
Items carry a value; the group carries the selection. Without multiple, selecting one item
unselects the rest.
Outline
Use variant="outline" for an outline style. The group hands the variant to every item, so it is
set once on the root.
Size
Use the size prop to change the size of the toggle group.
Spacing
Use spacing to add spacing between toggle group items. It is a spacing step, not a class, and it
defaults to 2. Pass spacing={0} for a joined control: the items lose their inner radii and, in
the outline variant, their shared borders collapse to one hairline.
Vertical
Use orientation="vertical" for vertical toggle groups.
Disabled
Disable the whole group from the root — every item inherits it.
Custom
Items take their own className, so a group can become a picker of swatches, densities or, here,
font weights. Upstream wraps this example in Field; this one keeps the
label and description as plain markup so the item styling is the only thing on show.
Use font-normal to set the font weight.
RTL
The row, the seams and the first/last radii are written in logical properties, so a right-to-left document mirrors them without any extra work.
Always one selected
A single-select group can normally be emptied by pressing the pressed item. Pass
deselectable={false} (API-23) for a toggle set that must always keep one option on (an alignment
or a text-size choice): that one press is ignored — the item stays pressed and onValueChange is
not called with []. Choosing another item still works. It has no effect with multiple. A switch
between VIEWS (list, board, calendar) is not a toggle group — use Tabs, or a ListToolbar's Layout tool.
Many options
Pass wrap (API-23) when the options can outgrow the row: the items wrap onto new lines instead of
widening the page. A joined group (spacing={0}) keeps each row a segment of its own, with rounded
ends and a leading border on every row, and a small gap between rows so their borders never double
up. The arrow keys still run through the items in order, from the end of one row to the start of the
next.
Checkbox chips
For picking which values are allowed — a family's colour temperatures, the finishes a generator
may combine — use a multiple group with wrap, variant="outline" and size="sm": each option
is a chip that toggles, pressed chips carry a check, and the chips wrap onto rows. Put a label row
above it with the count ("2 of 6") and two ghost xs buttons, Select all and Clear, each
disabled when it would do nothing. Name the group from the label with aria-labelledby. No
separate component: this is the whole pattern.
<ToggleGroup
aria-labelledby={labelId}
variant="outline"
size="sm"
spacing={1}
wrap
multiple
value={allowed}
onValueChange={setAllowed}
>
{options.map((option) => (
<ToggleGroupItem key={option} value={option}>
{allowed.includes(option) ? <CheckIcon data-icon="inline-start" /> : null}
{option}
</ToggleGroupItem>
))}
</ToggleGroup>API Reference
ToggleGroup
| Prop | Type | Default | Description |
|---|---|---|---|
deselectable | boolean | — | |
orientation | "horizontal" | "vertical" | 'horizontal' | |
size | "default" | "lg" | "sm" | null | — | |
spacing | number | — | |
variant | "default" | "outline" | null | — | |
wrap | boolean | — |
ToggleGroupItem
| Prop | Type | Default | Description |
|---|---|---|---|
size | "default" | "lg" | "sm" | null | — | |
variant | "default" | "outline" | null | — |
Accessibility
- The root renders
role="group"; each item is a native<button>carryingaria-pressed, so the selection is announced per item rather than as a list position. - Tab moves into the group, the arrow keys move between items
(looping at the ends, and across rows in a wrapped group), and
Enter or Space flips the focused item. With
deselectable={false}, flipping the pressed item of a single-select group does nothing. - Icon-only items have no visible text — give each one an
aria-label, as every example here does. - The group itself has no implicit name. Point
aria-labeloraria-labelledbyat the visible label when one exists, as the Custom example does. - Items keep the
togglerecipe's focus behaviour: the global 2px:focus-visibleoutline frombase.css, raised over its neighbours byfocus:z-10so a joined group never clips it.
| Key | Action |
|---|---|
| Tab | Move focus into and out of the group. |
| ← / → | Move focus to the previous or next item, looping at the ends. |
| ↑ / ↓ | The same, in a group with orientation="vertical". |
| Home / End | Move focus to the first or last item. |
| Enter / Space | Toggle the focused item. |
| Contract | States tested |
|---|---|
| Behaviour | default, pressed |
| Accessibility | focus-visible, labeled |
| Visual | default, focus |
Do / Don't
Deviations
Upstream's file plus packages/ui/upstream/patches/toggle-group.patch. Every hunk:
- A11Y-20 —
orientationreaches the Base UI group, so a vertical group moves with ↑/↓. Upstream only styles the column and keeps the keys horizontal. - API-23 —
ToggleGrouptakesdeselectable(defaulttrue;falsecancels the change that would empty a single-select group, so a view switch always has a view) andwrap(the items wrap onto rows; a joined group marks each row's first and last item so every row gets its own rounded ends, leading border and row gap). - DOC-2 —
cnis imported from@vegastack/design. Everything else — the recipe, the context, the spacing seams and the orientation classes — is upstream's, and the item's styling is upstream'stoggleVariantsre-used from thetoggleitem, so a pressed item takes Toggle's API-39 fill.