# Toggle Group

Pressable toggles for formatting bars, single or multiple. Each press pops a filled pill in or out.

**Category:** Inputs  
**Exports:** `ToggleGroup`  
**npm dependencies:** motion (+ clsx, tailwind-merge for `cn`)

## Install

```bash
npx shadcn@latest add https://charcoalui.com/r/toggle-group.json
```
The file lands at `components/ui/toggle-group.tsx`; import from `@/components/ui/toggle-group`.

## API — Toggle Group
| Prop | Type | Default | Notes |
| --- | --- | --- | --- |
| `items` | `{ value: string; label: string; icon?: ReactNode }[]` | — |  |
| `type` | `"single" \| "multiple"` | `"multiple"` |  |
| `value / defaultValue` | `string[]` | — |  |
| `iconOnly` | `boolean` | `false` |  |

## Example — Toggle Group

```tsx
import { AlignCenter, AlignLeft, AlignRight, Bold, Italic, Underline } from "lucide-react";
import { ToggleGroup } from "@/components/ui/toggle-group";

export default function ToggleGroupDemo({ iconOnly = true, type = "single" }: { iconOnly?: boolean; type?: "single" | "multiple" }) {
  return (
    <div className="flex flex-col items-center gap-4">
      <ToggleGroup
        aria-label="Text style"
        iconOnly={iconOnly}
        defaultValue={["bold"]}
        items={[
          { value: "bold", label: "Bold", icon: <Bold /> },
          { value: "italic", label: "Italic", icon: <Italic /> },
          { value: "underline", label: "Underline", icon: <Underline /> },
        ]}
      />
      <ToggleGroup
        aria-label="Alignment"
        type={type}
        defaultValue={["left"]}
        items={[
          { value: "left", label: "Left", icon: <AlignLeft /> },
          { value: "center", label: "Center", icon: <AlignCenter /> },
          { value: "right", label: "Right", icon: <AlignRight /> },
        ]}
      />
    </div>
  );
}
```

## Source — components/ui/toggle-group.tsx

```tsx
"use client";

import * as React from "react";
import { motion, useReducedMotion } from "motion/react";
import { cn } from "@/lib/utils";

export type ToggleItem = { value: string; label: string; icon?: React.ReactNode };

export type ToggleGroupProps = {
  items: ToggleItem[];
  type?: "single" | "multiple";
  value?: string[];
  defaultValue?: string[];
  onValueChange?: (value: string[]) => void;
  /** Show only icons; labels become accessible names and tooltips. */
  iconOnly?: boolean;
  className?: string;
  "aria-label"?: string;
};

export function ToggleGroup({ items, type = "multiple", value: valueProp, defaultValue = [], onValueChange, iconOnly = false, className, ...aria }: ToggleGroupProps) {
  const reduce = useReducedMotion();
  const [inner, setInner] = React.useState(defaultValue);
  const value = valueProp ?? inner;

  const toggle = (v: string) => {
    const on = value.includes(v);
    const next = type === "single" ? (on ? [] : [v]) : on ? value.filter((x) => x !== v) : [...value, v];
    if (valueProp === undefined) setInner(next);
    onValueChange?.(next);
  };

  return (
    <div role="group" {...aria} className={cn("inline-flex items-center gap-1 rounded-xl border bg-card p-1", className)}>
      {items.map((item) => {
        const on = value.includes(item.value);
        return (
          <motion.button
            key={item.value}
            type="button"
            aria-pressed={on}
            aria-label={iconOnly ? item.label : undefined}
            title={iconOnly ? item.label : undefined}
            onClick={() => toggle(item.value)}
            whileTap={reduce ? undefined : { scale: 0.92 }}
            className={cn(
              "relative inline-flex h-9 cursor-pointer items-center justify-center gap-1.5 rounded-lg text-sm font-medium outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring [&_svg]:size-4",
              iconOnly ? "w-9" : "px-3",
              on ? "text-background" : "text-muted-foreground hover:bg-accent hover:text-foreground",
            )}
          >
            <motion.span
              aria-hidden
              className="absolute inset-0 rounded-lg bg-foreground"
              initial={false}
              animate={{ opacity: on ? 1 : 0, scale: on ? 1 : 0.7 }}
              transition={reduce ? { duration: 0 } : { type: "spring", stiffness: 600, damping: 30 }}
            />
            <span className="relative inline-flex items-center gap-1.5">
              {item.icon}
              {!iconOnly && item.label}
            </span>
          </motion.button>
        );
      })}
    </div>
  );
}
```