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

Usage

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

  1. Compound API, Radix-compatible. <ToggleGroup.Root> is also re-exported as the bare <ToggleGroup>. Each <ToggleGroup.Item> is a real <button> with aria-pressed and data-state="on" | "off".
  2. Single + multiple modes. type="single" carries a string (re-pressing the active item clears it). type="multiple" carries a string[] and toggles membership. The onValueChange signature is discriminated by type.
  3. 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.
  4. Variant + size. Two variants (default, outline) and three sizes (sm, default, lg). The group sets the shared variant + size; individual items can override.
  5. Token-only theming. Pressed fill, hover, border, focus halo, and disabled opacity all read from --cb-* tokens.

Props

<ToggleGroup> / <ToggleGroup.Root>

PropTypeDefaultDescription
type"single" | "multiple""single"Selection mode. single carries a string, multiple carries a string[].
valuestring | string[]Controlled selected value. Pair with onValueChange.
defaultValuestring | string[]Uncontrolled initial value.
onValueChange(value) => voidFired 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.
disabledbooleanfalseDisables every item in the group at once.
classNamestringMerged onto the underlying <div role="group">.

<ToggleGroup.Item>

PropTypeDefaultDescription
valuestringThe value this item represents. Required.
disabledbooleaninherits groupDisables only this item.
variant"default" | "outline"inherits groupPer-item variant override.
size"sm" | "default" | "lg"inherits groupPer-item size override.
classNamestringMerged onto the underlying <button>.

Accessibility

  • The root renders as <div role="group"> with aria-orientation and aria-disabled reflecting the group props.
  • Each item renders as <button> with aria-pressed="true" | "false" and data-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 sets cursor-not-allowed.
  • Color contrast: pressed uses --cb-accent-fg on --cb-accent; unpressed uses --cb-fg on the surrounding surface. Both pass WCAG AA in the default theme.

Credits

  • Extracted from: algoflashcards (src/platform/ui/toggle-group.tsx). The source wrapped radix-ui's ToggleGroup primitives and pulled per-item visual variants from a sibling toggle.tsx via class-variance-authority. craft-bits drops both the radix-ui and cva dependencies — 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 compound ToggleGroup.{Root,Item} with a clean single/multiple type discriminator, a three-tier size scale, and orientation-aware keyboard navigation.