Toggle Group
Coordinated cluster of Toggles - radio-like with `type="single"` or checkbox-like with `type="multiple"`, with shared sizing and variant via context.
Overview
ToggleGroup is a coordinated set of Toggle-style buttons. Use type="single" for radio-like behaviour (one pressed at a time) or type="multiple" for checkbox-like (any number pressed). Sizes and variants propagate from the root to every ToggleGroupItem via context, so the cluster always looks coherent.
Use ToggleGroup for formatting and view state (text alignment, view mode, filter chips) - icon-led, immediate visual effect. RadioGroup is for form values (label-led, captured on submit); Tabs swap full content panels rather than apply a state.
Preview
Installation
bash npx gremorie@latest add rx-toggle-group bash pnpm dlx gremorie@latest add rx-toggle-group
bash yarn dlx gremorie@latest add rx-toggle-group
bash bunx --bun gremorie@latest add rx-toggle-group
Usage
import { ToggleGroup, ToggleGroupItem } from "@gremorie/rx-forms";
import { Bold, Italic, Underline } from "lucide-react";
export function Example() {
return (
<ToggleGroup type="single" defaultValue="bold">
<ToggleGroupItem value="bold" aria-label="Bold">
<Bold />
</ToggleGroupItem>
<ToggleGroupItem value="italic" aria-label="Italic">
<Italic />
</ToggleGroupItem>
<ToggleGroupItem value="underline" aria-label="Underline">
<Underline />
</ToggleGroupItem>
</ToggleGroup>
);
}Angular edition planned for Phase 5h. Star the repo to track progress.
API
<ToggleGroup>
| Prop | Type | Default | Description |
|---|---|---|---|
type | "single" | "multiple" | - | single enforces one pressed item; multiple allows any number. Required. |
value | string | string[] | - | Controlled value. Single string for type="single", array for type="multiple". |
defaultValue | string | string[] | - | Uncontrolled initial value. |
onValueChange | (value: string | string[]) => void | - | Fires when selection changes. |
disabled | boolean | false | Disables the whole group. |
variant | "default" | "outline" | "default" | Forwarded to every item via context. |
size | "default" | "sm" | "lg" | "default" | Forwarded to every item via context. |
spacing | number | 0 | Gap between items in spacing units. 0 joins them with a shared border (button-group style); positive values break them into separate buttons. |
rovingFocus | boolean | true | Enables roving tabindex (Radix default). |
loop | boolean | true | Arrow keys wrap. |
Forwards to ToggleGroupPrimitive.Root. Wraps children in an internal ToggleGroupContext so ToggleGroupItem inherits variant, size, and spacing.
<ToggleGroupItem>
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | - | The value reported back to the parent. Required. |
disabled | boolean | false | Disables this single item. |
variant | "default" | "outline" | inherited from group | Override the group's variant for this item. Rare. |
size | "default" | "sm" | "lg" | inherited from group | Override the group's size for this item. Rare. |
The item reads from ToggleGroupContext before falling back to its own props, so the group always wins unless explicitly overridden.
Composition
<ToggleGroup>is the root context. Settype,variant,size, andspacingonce.<ToggleGroupItem>always inside<ToggleGroup>- it depends on the context for styling.- Children are typically icons. For text + icon, follow the
Buttonpattern. spacing={0}(default) joins items with a shared border likeButtonGroup. Positivespacingkeeps them as separate buttons with gaps.
Variations
Text formatting (single)
The canonical pattern. type="single" for radio-like behaviour.
import { AlignLeft, AlignCenter, AlignRight } from 'lucide-react';
<ToggleGroup type="single" defaultValue="left">
<ToggleGroupItem value="left" aria-label="Align left">
<AlignLeft />
</ToggleGroupItem>
<ToggleGroupItem value="center" aria-label="Align center">
<AlignCenter />
</ToggleGroupItem>
<ToggleGroupItem value="right" aria-label="Align right">
<AlignRight />
</ToggleGroupItem>
</ToggleGroup>;Multi-select formatting
type="multiple" lets users press multiple items (e.g. bold + italic at the same time).
import { Bold, Italic, Underline } from 'lucide-react';
<ToggleGroup type="multiple" defaultValue={['bold']}>
<ToggleGroupItem value="bold" aria-label="Bold">
<Bold />
</ToggleGroupItem>
<ToggleGroupItem value="italic" aria-label="Italic">
<Italic />
</ToggleGroupItem>
<ToggleGroupItem value="underline" aria-label="Underline">
<Underline />
</ToggleGroupItem>
</ToggleGroup>;Outline variant with spacing
When the cluster should read as a row of independent buttons rather than one joined unit, use variant="outline" plus a positive spacing value.
import { List, LayoutGrid, Rows3 } from 'lucide-react';
<ToggleGroup type="single" defaultValue="grid" variant="outline" spacing={1}>
<ToggleGroupItem value="list" aria-label="List view">
<List />
</ToggleGroupItem>
<ToggleGroupItem value="grid" aria-label="Grid view">
<LayoutGrid />
</ToggleGroupItem>
<ToggleGroupItem value="board" aria-label="Board view">
<Rows3 />
</ToggleGroupItem>
</ToggleGroup>;Controlled selection
Drive the cluster from external state - useful for filter chips backed by URL search params.
function FilterChips() {
const [filters, setFilters] = React.useState<string[]>([]);
return (
<ToggleGroup
type="multiple"
value={filters}
onValueChange={setFilters}
variant="outline"
>
<ToggleGroupItem value="open">Open</ToggleGroupItem>
<ToggleGroupItem value="closed">Closed</ToggleGroupItem>
<ToggleGroupItem value="merged">Merged</ToggleGroupItem>
</ToggleGroup>
);
}Accessibility
- Group semantics: rendered with
role="group". Items carry the appropriaterole="radio"(fortype="single") orrole="button"witharia-pressed(fortype="multiple"). - Roving tabindex: only one item is in the tab order at a time.
Tabmoves out of the group; arrow keys move within. - Keyboard:
Tabenters and exits the group.ArrowLeft/ArrowRight(orArrowUp/ArrowDown) move between items.Home/Endjump to first / last.Space/Entertoggle the focused item.
- Loop: arrow keys wrap by default. Pass
loop={false}to disable. - Icon-only items: always provide
aria-labelon each item. The group itself can also carryaria-labelto name the entire cluster.
Related
- Toggle - the underlying single-button primitive
- Button Group - visual cousin without coordinated state
- Radio Group - label-led single-select for form values
- Tabs - swap full content panels (different mental model)