Skip to content
Component installs need the registry setup
VegaStack Design

Toggle Group

A set of two-state buttons sharing one selection — single or multiple, three sizes, adjustable spacing, and a vertical axis.

Status
stable
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-group

Install with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.

pnpm dlx shadcn@4.21.0 add @vegastack/toggle-group

Run 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 — data-slot="toggle-group"
ToggleGroupItem — data-slot="toggle-group-item"
ToggleGroup
├── ToggleGroupItem
└── ToggleGroupItem
  • ToggleGroup — the role="group" container (data-slot="toggle-group"). Owns the selection (value, defaultValue, onValueChange, multiple), the shared variant and size it passes to every item through context, and the spacing and orientation that shape the row.
  • ToggleGroupItem — one native toggle button (data-slot="toggle-group-item"), identified by its value and styled by toggleVariants from the toggle item. It reads variant, size and spacing from 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.

Font Weight

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.

Colour temperature2 of 6
Trim7 of 7
<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

PropTypeDefaultDescription
deselectableboolean—
orientation"horizontal" | "vertical"'horizontal'
size"default" | "lg" | "sm" | null—
spacingnumber—
variant"default" | "outline" | null—
wrapboolean—

ToggleGroupItem

PropTypeDefaultDescription
size"default" | "lg" | "sm" | null—
variant"default" | "outline" | null—

Accessibility

  • The root renders role="group"; each item is a native <button> carrying aria-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-label or aria-labelledby at the visible label when one exists, as the Custom example does.
  • Items keep the toggle recipe's focus behaviour: the global 2px :focus-visible outline from base.css, raised over its neighbours by focus:z-10 so a joined group never clips it.
KeyAction
TabMove 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 / EndMove focus to the first or last item.
Enter / SpaceToggle the focused item.
ContractStates tested
Behaviourdefault, pressed
Accessibilityfocus-visible, labeled
Visualdefault, focus

Do / Don't

Do
Use ToggleGroup for pressed action choices; set variant and size once on the group. Use Tabs for toolbar scopes and ListToolbar's Layout tool for Grid | List | Board.
Don't
Use a toggle group for a form value (a Radio Group), for swapping page regions (Tabs), or for moving between URLs (links).

Deviations

Upstream's file plus packages/ui/upstream/patches/toggle-group.patch. Every hunk:

  • A11Y-20 — orientation reaches the Base UI group, so a vertical group moves with ↑/↓. Upstream only styles the column and keeps the keys horizontal.
  • API-23 — ToggleGroup takes deselectable (default true; false cancels the change that would empty a single-select group, so a view switch always has a view) and wrap (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 — cn is 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's toggleVariants re-used from the toggle item, so a pressed item takes Toggle's API-39 fill.

On this page