# Switch

A spring-driven thumb that stretches while pressed, like a real toggle under your thumb.

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

## Install

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

## API — Switch
| Prop | Type | Default | Notes |
| --- | --- | --- | --- |
| `checked` | `boolean` | — |  |
| `defaultChecked` | `boolean` | `false` |  |
| `onCheckedChange` | `(checked: boolean) => void` | — |  |
| `size` | `"sm" \| "md"` | `"md"` |  |
| `disabled` | `boolean` | — |  |

## Example — Switch

```tsx
import * as React from "react";
import { Switch } from "@/components/ui/switch";

export default function SwitchDemo({ size = "md", disabled = false }: { size?: "sm" | "md"; disabled?: boolean }) {
  const [on, setOn] = React.useState(true);
  return (
    <div className="grid w-full max-w-xs gap-4">
      <label className="flex items-center justify-between gap-4 rounded-xl border bg-background p-4">
        <span>
          <span className="block text-sm font-medium">Weekly digest</span>
          <span className="block text-[13px] text-muted-foreground">{on ? "Sent every Monday" : "Paused"}</span>
        </span>
        <Switch checked={on} onCheckedChange={setOn} size={size} disabled={disabled} aria-label="Weekly digest" />
      </label>
      <label className="flex items-center justify-between gap-4 rounded-xl border bg-background p-4">
        <span className="text-sm font-medium">Compact mode</span>
        <Switch size="sm" disabled={disabled} aria-label="Compact mode" />
      </label>
    </div>
  );
}
```

## Source — components/ui/switch.tsx

```tsx
"use client";

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

export type SwitchProps = Omit<React.ComponentProps<"button">, "onChange" | "value"> & {
  checked?: boolean;
  defaultChecked?: boolean;
  onCheckedChange?: (checked: boolean) => void;
  size?: "sm" | "md";
};

const dims = {
  sm: { w: 36, h: 20, thumb: 16, stretch: 4 },
  md: { w: 46, h: 26, thumb: 20, stretch: 6 },
};

export function Switch({ checked: checkedProp, defaultChecked = false, onCheckedChange, size = "md", disabled, className, ...props }: SwitchProps) {
  const reduce = useReducedMotion();
  const [inner, setInner] = React.useState(defaultChecked);
  const [pressed, setPressed] = React.useState(false);
  const checked = checkedProp ?? inner;
  const d = dims[size];
  const pad = (d.h - d.thumb) / 2;
  const thumbW = pressed && !reduce ? d.thumb + d.stretch : d.thumb;
  const x = checked ? d.w - pad - thumbW : pad;

  const toggle = () => {
    if (checkedProp === undefined) setInner(!checked);
    onCheckedChange?.(!checked);
  };

  return (
    <button
      type="button"
      role="switch"
      aria-checked={checked}
      disabled={disabled}
      onClick={toggle}
      onPointerDown={() => setPressed(true)}
      onPointerUp={() => setPressed(false)}
      onPointerLeave={() => setPressed(false)}
      className={cn(
        "relative inline-flex shrink-0 cursor-pointer rounded-full outline-none transition-colors duration-200",
        "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
        "disabled:cursor-not-allowed disabled:opacity-50",
        checked ? "bg-foreground" : "bg-input",
        className,
      )}
      style={{ width: d.w, height: d.h }}
      {...props}
    >
      <motion.span
        aria-hidden
        className={cn("absolute rounded-full shadow-[0_1px_3px_rgb(0_0_0/0.3)]", checked ? "bg-background" : "bg-foreground/90 dark:bg-foreground")}
        initial={false}
        animate={{ x, width: thumbW }}
        transition={reduce ? { duration: 0 } : { type: "spring", stiffness: 600, damping: 32 }}
        style={{ top: pad, left: 0, height: d.thumb }}
      />
    </button>
  );
}
```