# Toggle

A two-state button for toolbars: pressing fills the background from the centre and gives the icon a small pop. Exposes aria-pressed.

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

## Install

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

## API — Toggle
| Prop | Type | Default | Notes |
| --- | --- | --- | --- |
| `pressed / defaultPressed` | `boolean` | `false` |  |
| `onPressedChange` | `(pressed: boolean) => void` | — |  |
| `label` | `string` | — | For icon-only toggles. |
| `size` | `"sm" \| "md" \| "lg"` | `"md"` |  |
| `children` | `ReactNode` | — |  |
| `className` | `string` | — |  |

## Example — Toggle

```tsx
import { Bold, Italic, Underline } from "lucide-react";
import { Toggle } from "@/components/ui/toggle";

export default function ToggleDemo({ size = "md" }: { size?: "sm" | "md" | "lg" }) {
  return (
    <div className="flex gap-1 rounded-xl border bg-card p-1">
      <Toggle size={size} label="Bold" defaultPressed>
        <Bold />
      </Toggle>
      <Toggle size={size} label="Italic">
        <Italic />
      </Toggle>
      <Toggle size={size} label="Underline">
        <Underline />
      </Toggle>
    </div>
  );
}
```

## Source — components/ui/toggle.tsx

```tsx
"use client";

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

export type ToggleProps = {
  pressed?: boolean;
  defaultPressed?: boolean;
  onPressedChange?: (pressed: boolean) => void;
  /** Read by screen readers when the content is only an icon. */
  label?: string;
  size?: "sm" | "md" | "lg";
  children: React.ReactNode;
  className?: string;
};

const sizes = { sm: "h-8 min-w-8 px-2 text-xs", md: "h-9 min-w-9 px-2.5 text-sm", lg: "h-11 min-w-11 px-3 text-sm" };

/** A two-state button: pressing it fills the background from the centre and gives the icon a small pop. */
export function Toggle({ pressed: pressedProp, defaultPressed = false, onPressedChange, label, size = "md", children, className }: ToggleProps) {
  const reduce = useReducedMotion();
  const [inner, setInner] = React.useState(defaultPressed);
  const pressed = pressedProp ?? inner;

  return (
    <motion.button
      type="button"
      aria-pressed={pressed}
      aria-label={label}
      onClick={() => {
        setInner(!pressed);
        onPressedChange?.(!pressed);
      }}
      whileTap={reduce ? undefined : { scale: 0.92 }}
      className={cn("relative inline-flex cursor-pointer items-center justify-center gap-1.5 overflow-hidden rounded-lg font-medium outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring", pressed ? "text-foreground" : "text-muted-foreground hover:bg-accent hover:text-foreground", sizes[size], className)}
    >
      <motion.span aria-hidden className="absolute inset-0 rounded-[inherit] bg-accent ring-1 ring-inset ring-foreground/10" initial={false} animate={{ scale: pressed ? 1 : 0.4, opacity: pressed ? 1 : 0 }} transition={{ type: "spring", stiffness: 500, damping: 30 }} />
      <motion.span className="relative inline-flex items-center gap-1.5 [&_svg]:size-4" animate={reduce ? undefined : { scale: pressed ? [1, 1.18, 1] : 1 }} transition={{ duration: 0.28 }}>
        {children}
      </motion.span>
    </motion.button>
  );
}
```