Toggle group
Exclusive views in a stroked well, or a cluster of flags.
Toggle group is a row of toggles that works as one
control, for picking the view out of several, like List or Board, or for a set of flags that
each stay on or off. By default the items sit in a sunken well and the one that is on is
raised, and density="dots" shrinks them to dots that open into icons on hover. It changes
the view you are already in and owns no panels, which is what sets it apart from
Tabs.
Usage
import { List, SquaresFour } from '@phosphor-icons/react';
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
<ToggleGroup type="single" value={view} onValueChange={setView}>
<ToggleGroupItem value="list">
<List />
List
</ToggleGroupItem>
<ToggleGroupItem value="board">
<SquaresFour />
Board
</ToggleGroupItem>
</ToggleGroup>The well
The default variant="well" is a sunken track on bg-surface-sunken with a stroke, and it
follows the look's control corner, 4px rounder than the items inside it, so it is a pill where
buttons are pills, as in ICS, and nearly square in Vistula. The item that is on rises onto bg-surface-raised with shadow-sm
with its label in text-fg and its icon in text-primary, and the others stay text-fg-muted until hovered. The well is
40px tall and sizes its own items, so it ignores size and spacing, and an icon inside is
16px.
One or several
type="single" keeps exactly one item on. Pressing the item that is already on does nothing,
so the group never falls back to empty and onValueChange only ever receives a value. Give it
a value or a defaultValue, so one item is on from the start.
type="multiple" takes an array of values in the same well. Each item turns on and off by
itself, so any number of them can be raised, including none.
Dots for a row of icons
density="dots" shrinks each item in the well to an 8px dot, with the one that is on in
bg-primary. While the pointer is over the group or focus is inside it, the dots open into
28px icon buttons, with the selected icon in text-primary. A touch screen has no hover, so
there the group stays open with 44px items. Put only an icon in each item, with an
aria-label, and the label names the item and shows as a tooltip below it.
<ToggleGroup type="single" density="dots" value={view} onValueChange={setView}>
<ToggleGroupItem value="list" aria-label="List">
<List />
</ToggleGroupItem>
<ToggleGroupItem value="board" aria-label="Board">
<SquaresFour />
</ToggleGroupItem>
</ToggleGroup>Joined or spaced
With variant="default" or variant="outline", each item is drawn as a Toggle instead, with
its on state in bg-fill and its height from size. At the default spacing={0} the items
join into one control with square inner corners, and outline items share the border between
them. Set spacing to a number of pixels to pull them apart into separate toggles.
Props
Toggle group takes every prop of the Radix Toggle group, such as type, value,
defaultValue, onValueChange, disabled, and orientation, plus these. ToggleGroupItem
needs a value and takes no variant or size of its own, so set those on the group.
| Prop | Type | Default | What it does |
|---|---|---|---|
variant | "well" | "default" | "outline" | "well" | well is the sunken track with a raised item. default and outline draw each item as a Toggle. |
density | "default" | "dots" | "default" | With the well, dots shrinks the items to dots that open into icons on hover and focus. |
size | "sm" | "default" | "lg" | "default" | 32, 36, or 40px tall for default and outline items. The well ignores it. |
spacing | number | 0 | The gap between default and outline items, in pixels. At 0 they join. The well ignores it. |
type="single" works as on Radix, except that it cannot be cleared, so onValueChange never
receives an empty string.
Accessibility
- With
type="single"the group is aradiogroupand each item is aradiowitharia-checked. Withtype="multiple"the group is atoolbarand each item is a toggle button witharia-pressed. - The group is one tab stop, and Tab lands on the item that is on. The arrow keys move between items and wrap at the ends, and Home and End jump to the first and last. Arrows only move focus, so Space or Enter is what turns an item on. Disabled items are skipped.
- An item's name is its text. An item with only an icon needs an
aria-label, and indotsthat label is also its tooltip. - Focus shows a 2px ring on
:focus-visibleonly, in the contrast ink, and the focused item rises above its neighbors so a joined row does not cover the ring. - On a touch screen each well item gets a 44px hit area around it without changing its size,
dots open to 44px items, and
defaultandoutlineitems are at least 44px tall. - Under reduced motion the raised item, the dots, and the icons change without animating, and the tooltip appears without a fade.
- In forced colors the item that is on has no visible mark, because the system replaces its
raised fill and drops its shadow, and the collapsed dots are fills too, so they do not show
until the group opens. Assistive tech still reports the state from
aria-checkedoraria-pressed.
When to use something else
- Tabs when each option owns a panel of content. Tabs gives its triggers and panels the tab roles and marks the active tab with a line on the list's edge, and Toggle group only reports a value.
- Tab switcher builds this same well, with dots
for icons, from a
tabsarray. Use Toggle group directly in new work. - Radio group for a choice that a form submits, with a label beside each option.
- Toggle for a single state on its own.
- Button group for actions joined into one control, where nothing stays on.
Install
shadcn add @gesso/toggle-group