Toggle Group

Free · MIT

Single (radiogroup) or multiple (aria-pressed) toggle buttons in outline, soft, solid, segmented, neo and glass styles with a sliding, fading or popping active marker, text or icon items, tones, sizes, shapes, shadows, attached or spaced, vertical, full width, disabled items and roving arrow-key focus that flips in RTL.

MarkdownOpen in v0
Open in v0

Installation

$ npx shadcn@latest add https://goodui.dev/r/toggle-group.json

The file lands in components/goodui/toggle-group.tsx and is yours to edit. With the @goodui registry added, npx shadcn@latest add @goodui/toggle-group works too.

Usage

import * as React from "react"
import { CalendarDays, CalendarRange, LayoutList } from "lucide-react"
import { ToggleGroup, ToggleGroupItem } from "@/components/goodui/toggle-group"

export function CalendarToolbar() {
  const [view, setView] = React.useState<string | null>("week")
  const [days, setDays] = React.useState<string[]>(["Mon", "Tue", "Wed"])

  return (
    <div className="flex flex-wrap gap-4">
      <ToggleGroup
        aria-label="Calendar view"
        variant="segmented"
        value={view}
        onValueChange={setView}
        items={[
          { value: "day", label: "Day", icon: <CalendarDays /> },
          { value: "week", label: "Week", icon: <CalendarRange /> },
          { value: "agenda", label: "Agenda", icon: <LayoutList /> },
        ]}
      />
      <ToggleGroup type="multiple" aria-label="Working days" value={days} onValueChange={setDays}>
        {["Mon", "Tue", "Wed", "Thu", "Fri"].map((d) => (
          <ToggleGroupItem key={d} value={d}>
            {d}
          </ToggleGroupItem>
        ))}
      </ToggleGroup>
    </div>
  )
}

When to use

Use it for
  • Switching a view or mode in place: Day / Week / Month, list / grid, monthly / yearly billing.
  • Picking one of two to five short options that should all stay visible (single).
  • Turning several independent options on and off in one row: working days, filters, text formatting (multiple).
Reach for something else
  • Switching between panels of content: use tabs, which wires tabpanels and aria-controls.
  • More than about six options or long labels: use select.
  • A single on/off setting: use switch.
  • Navigation between pages: use links.
If you needUse
iOS-style segmented controlvariant="segmented" (or "solid" for a filled marker)
Toolbar of joined buttonsvariant="outline" (attached by default)
Light filter chipsvariant="soft" shape="pill"
Several valuestype="multiple" (aria-pressed buttons)
Icon-only buttonsitems with icon and aria-label (title adds a tooltip)
On photos or gradientsvariant="glass"
Playful or retro brandsvariant="neo"; attached={false} spaces the buttons

Examples

Billing period

<ToggleGroup variant="segmented" shape="pill" aria-label="Billing period" defaultValue="yearly"
  items={[{ value: "monthly", label: "Monthly" }, { value: "yearly", label: "Yearly −20%" }]} />

Icon-only layout switch

<ToggleGroup variant="outline" tone="neutral" size="sm" aria-label="Layout" defaultValue="list"
  items={[
    { value: "list", icon: <List />, "aria-label": "List", title: "List" },
    { value: "grid", icon: <LayoutGrid />, "aria-label": "Grid", title: "Grid" },
  ]} />

Clearable single choice

<ToggleGroup deselectable aria-label="Reminder" items={[{ value: "sms", label: "SMS" }, { value: "email", label: "Email" }]} />

API reference

ToggleGroup

PropTypeDefault
items
Shortcut: renders a ToggleGroupItem per option. Or pass <ToggleGroupItem> children.
ToggleGroupOption[]—
variant
How the group is painted. outline and neo draw joined buttons; soft, solid, segmented and glass sit in a track.
"outline" | "soft" | "solid" | "segmented" | "neo" | "glass"—
tone
Color of the active item.
"primary" | "neutral" | "success" | "warning" | "danger" | "info"—
size"sm" | "md" | "lg"—
shape"square" | "rounded" | "pill"—
shadow
Group shadow; glow puts a tone-colored glow under the active marker instead.
"none" | "sm" | "md" | "glow"—
animation
slide glides one marker between items (single only; multiple falls back to fade). Reduced motion snaps.
"none" | "slide" | "fade" | "pop"—
orientation"horizontal" | "vertical"—
attached
outline and neo: buttons share borders in one box. false spaces them apart.
boolean—
fullWidth
Items share the full width equally.
boolean—
disabled
Disables every item.
boolean—
name
Form field name (renders hidden inputs with the selected values).
string—
type
single: one value (a radiogroup). multiple: any number (each item has aria-pressed).
"single"—
value
Selected value (controlled): a string or null; string[] with type="multiple".
string | null—
defaultValue
Initial value when uncontrolled.
string | null—
onValueChange
Called with the new value (string | null, or string[] with type="multiple").
(value: string | null) => void—
deselectable
single: clicking the active item clears the value.
boolean—

Also accepts every <div> attribute.

ToggleGroupItem

PropTypeDefault
value*
Value reported to the group.
string—
icon
Icon before the label; with no label the item becomes square (pass aria-label).
React.ReactNode—

Also accepts every <button> attribute.

Accessibility

TabEnters the group on the selected item (single) or the last focused item (multiple); one tab stop.
→ / ↓Next enabled item, wrapping (← in RTL). In single mode it also selects it.
← / ↑Previous enabled item, wrapping (→ in RTL). In single mode it also selects it.
Home / EndFirst / last enabled item.
Space / EnterToggles the focused item (multiple) or selects it (single; clears it with deselectable).
  • type="single" renders role="radiogroup" with role="radio" items and aria-checked (the radio pattern: arrows move and select).
  • type="multiple" renders role="group" with native buttons and aria-pressed; arrows only move focus.
  • Give the group a name with aria-label or aria-labelledby. Icon-only items need an aria-label; the icon is decorative.
  • Disabled items are skipped by the arrow keys and keep the native disabled attribute.
  • name renders hidden inputs with the selected values for native forms.

Motion

  • slide (single): one marker glides between items with a motion layout spring (stiffness 520, damping 40) and keeps its corner radius while it stretches.
  • fade: the marker fades and scales in from 0.94 over 200 ms. pop: it springs in from 0.6 over 340 ms. Both run only after the first interaction.
  • Items scale to 0.96 while pressed (neo moves into its shadow instead). none is static.
  • Reduced motion: the marker snaps (no layout animation), and press, fade and pop are off.
  • The roving tab stop is computed from the DOM after each render without extra renders unless it changes. Multiple mode never uses layout animations.

Notes for AI

Coding agents read these rules through MCP and llms.txt, or paste this page with Copy page.

  • value is a string (or null) for single and string[] for type="multiple"; onValueChange matches.
  • Use items for simple lists; use ToggleGroupItem children when you need custom content per item.
  • Do not use it to show different content panels; that is tabs.
  • Single mode cannot be emptied by clicking unless deselectable is set, like radio buttons.
  • variant, tone, size, shape, shadow, animation, orientation and attached are independent layers; combine them instead of adding color classes.

FAQ

Radio semantics or aria-pressed?

Single uses the radio pattern because exactly one value is chosen; multiple uses aria-pressed toggle buttons because each item is independent. Both follow the WAI-ARIA APG patterns.

Why is my marker not sliding?

The sliding marker is single-mode only and turns off with reduced motion. Multiple mode fades or pops each active item.

Related