Skip to main content
Gremorie

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>

PropTypeDefaultDescription
type"single" | "multiple"-single enforces one pressed item; multiple allows any number. Required.
valuestring | string[]-Controlled value. Single string for type="single", array for type="multiple".
defaultValuestring | string[]-Uncontrolled initial value.
onValueChange(value: string | string[]) => void-Fires when selection changes.
disabledbooleanfalseDisables the whole group.
variant"default" | "outline""default"Forwarded to every item via context.
size"default" | "sm" | "lg""default"Forwarded to every item via context.
spacingnumber0Gap between items in spacing units. 0 joins them with a shared border (button-group style); positive values break them into separate buttons.
rovingFocusbooleantrueEnables roving tabindex (Radix default).
loopbooleantrueArrow keys wrap.

Forwards to ToggleGroupPrimitive.Root. Wraps children in an internal ToggleGroupContext so ToggleGroupItem inherits variant, size, and spacing.

<ToggleGroupItem>

PropTypeDefaultDescription
valuestring-The value reported back to the parent. Required.
disabledbooleanfalseDisables this single item.
variant"default" | "outline"inherited from groupOverride the group's variant for this item. Rare.
size"default" | "sm" | "lg"inherited from groupOverride 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

  1. <ToggleGroup> is the root context. Set type, variant, size, and spacing once.
  2. <ToggleGroupItem> always inside <ToggleGroup> - it depends on the context for styling.
  3. Children are typically icons. For text + icon, follow the Button pattern.
  4. spacing={0} (default) joins items with a shared border like ButtonGroup. Positive spacing keeps 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 appropriate role="radio" (for type="single") or role="button" with aria-pressed (for type="multiple").
  • Roving tabindex: only one item is in the tab order at a time. Tab moves out of the group; arrow keys move within.
  • Keyboard:
    • Tab enters and exits the group.
    • ArrowLeft / ArrowRight (or ArrowUp / ArrowDown) move between items.
    • Home / End jump to first / last.
    • Space / Enter toggle the focused item.
  • Loop: arrow keys wrap by default. Pass loop={false} to disable.
  • Icon-only items: always provide aria-label on each item. The group itself can also carry aria-label to name the entire cluster.
  • 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)

On this page