Toggle Group
A Radix-style toggle group primitive. A single value flows in, a single onValueChange flows out, and the group owns roving-tabindex plus arrow-key navigation across its items. Pick type="single" for a one-of-N pressable bar (alignment, view mode) or type="multiple" for an N-of-N pressable bar (text styles, filter chips).
Preview
Installation
npx shadcn@latest add https://craftbits.dev/r/toggle-group.jsonUsage
Single — exactly one item can be pressed at a time:
import { ToggleGroup } from "@craft-bits/core";
const [align, setAlign] = useState("left");
<ToggleGroup type="single" value={align} onValueChange={setAlign}>
<ToggleGroup.Item value="left">Left</ToggleGroup.Item>
<ToggleGroup.Item value="center">Center</ToggleGroup.Item>
<ToggleGroup.Item value="right">Right</ToggleGroup.Item>
</ToggleGroup>Multiple — any subset can be pressed at once:
const [styles, setStyles] = useState<string[]>(["bold"]);
<ToggleGroup type="multiple" value={styles} onValueChange={setStyles}>
<ToggleGroup.Item value="bold">Bold</ToggleGroup.Item>
<ToggleGroup.Item value="italic">Italic</ToggleGroup.Item>
<ToggleGroup.Item value="underline">Underline</ToggleGroup.Item>
</ToggleGroup>Uncontrolled — seed via defaultValue and let the group own the state:
<ToggleGroup type="single" defaultValue="left">
<ToggleGroup.Item value="left">Left</ToggleGroup.Item>
<ToggleGroup.Item value="center">Center</ToggleGroup.Item>
<ToggleGroup.Item value="right">Right</ToggleGroup.Item>
</ToggleGroup>Understanding the component
- Compound API, Radix-compatible.
<ToggleGroup.Root>is also re-exported as the bare<ToggleGroup>. Each<ToggleGroup.Item>is a real<button>witharia-pressedanddata-state="on" | "off". - Single + multiple modes.
type="single"carries astring(re-pressing the active item clears it).type="multiple"carries astring[]and toggles membership. TheonValueChangesignature is discriminated bytype. - Roving-tabindex + arrow keys. ArrowLeft / ArrowRight move along a horizontal group; ArrowUp / ArrowDown move along a vertical group. Home jumps to the first item, End to the last. Items wrap.
- Variant + size. Two variants (
default,outline) and three sizes (sm,default,lg). The group sets the shared variant + size; individual items can override. - Token-only theming. Pressed fill, hover, border, focus halo, and disabled opacity all read from
--cb-*tokens.
Props
<ToggleGroup> / <ToggleGroup.Root>
| Prop | Type | Default | Description |
|---|---|---|---|
type | "single" | "multiple" | "single" | Selection mode. single carries a string, multiple carries a string[]. |
value | string | string[] | — | Controlled selected value. Pair with onValueChange. |
defaultValue | string | string[] | — | Uncontrolled initial value. |
onValueChange | (value) => void | — | Fired with the next value on every user toggle. |
variant | "default" | "outline" | "default" | Shared variant for every item. |
size | "sm" | "default" | "lg" | "default" | Shared size for every item. |
orientation | "horizontal" | "vertical" | "horizontal" | Layout direction; also swaps the arrow-key axis. |
disabled | boolean | false | Disables every item in the group at once. |
className | string | — | Merged onto the underlying <div role="group">. |
<ToggleGroup.Item>
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | — | The value this item represents. Required. |
disabled | boolean | inherits group | Disables only this item. |
variant | "default" | "outline" | inherits group | Per-item variant override. |
size | "sm" | "default" | "lg" | inherits group | Per-item size override. |
className | string | — | Merged onto the underlying <button>. |
Accessibility
- The root renders as
<div role="group">witharia-orientationandaria-disabledreflecting the group props. - Each item renders as
<button>witharia-pressed="true" | "false"anddata-state="on" | "off". - Keyboard: Tab moves into the group once. ArrowLeft / ArrowRight (or ArrowUp / ArrowDown when vertical) step between enabled items with wrap-around. Home / End jump to the ends. Space / Enter toggle the focused item.
disabled(group or item) propagates to the button, removes it from arrow-key navigation, reduces opacity, and setscursor-not-allowed.- Color contrast: pressed uses
--cb-accent-fgon--cb-accent; unpressed uses--cb-fgon the surrounding surface. Both pass WCAG AA in the default theme.
Credits
- Extracted from:
algoflashcards(src/platform/ui/toggle-group.tsx). The source wrappedradix-ui'sToggleGroupprimitives and pulled per-item visual variants from a siblingtoggle.tsxviaclass-variance-authority. craft-bits drops both theradix-uiandcvadependencies — the group is a plain<div role="group">with its own roving-arrow logic, each item is a<button aria-pressed>with state-driven class hooks — and broadens the API into a compoundToggleGroup.{Root,Item}with a clean single/multipletypediscriminator, a three-tiersizescale, and orientation-aware keyboard navigation.