# Switch

> Toggle switch whose thumb slides on a stiff spring and stretches while pressed.

- Collection: Components
- Tier: free
- Docs: https://geturui.com/docs/switch
- Install: `npx shadcn@latest add @geturui/switch`
- Dependencies: motion

## Usage

```tsx
import { Switch } from "@/components/ui/switch";

export function EmailSettings() {
  return (
    <Switch
      label="Email me about releases"
      defaultChecked
      onCheckedChange={(on) => console.log("releases", on)}
    />
  );
}
```

## Props

| Prop | Type | Default | Description |
|---|---|---|---|
| checked | `boolean` | — | On state when controlled. |
| defaultChecked | `boolean` | false | Initial state when uncontrolled. |
| onCheckedChange | `(checked: boolean) => void` | — | Called with the new state. |
| label | `ReactNode` | — | Visible label; wraps the switch in a <label> so the text toggles it too. |
| disabled | `boolean` | false | Dims the switch and blocks input. |
| aria-label | `string` | — | Accessible name when there is no visible label. |
| className | `string` | — | Classes for the label wrapper, or the switch when there is no label. |

## Accessibility

- A native <button role="switch"> with aria-checked, toggled with Space or Enter.
- With label, the text is a real <label>, so clicking it toggles and it becomes the accessible name.
- Without a visible label, pass aria-label or wrap it in your own <label>.

## Source

### components/ui/switch.tsx

```tsx
"use client";

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

export interface SwitchProps {
  checked?: boolean;
  defaultChecked?: boolean;
  onCheckedChange?: (checked: boolean) => void;
  label?: React.ReactNode;
  disabled?: boolean;
  className?: string;
  "aria-label"?: string;
}

/** Toggle with a spring-driven thumb that stretches while pressed. */
export function Switch({ checked, defaultChecked = false, onCheckedChange, label, disabled, className, ...rest }: SwitchProps) {
  const [inner, setInner] = React.useState(defaultChecked);
  const [pressed, setPressed] = React.useState(false);
  const on = checked ?? inner;

  function toggle() {
    if (checked === undefined) setInner(!on);
    onCheckedChange?.(!on);
  }

  const control = (
    <button
      type="button"
      role="switch"
      aria-checked={on}
      aria-label={label ? undefined : rest["aria-label"]}
      disabled={disabled}
      onClick={toggle}
      onPointerDown={() => setPressed(true)}
      onPointerUp={() => setPressed(false)}
      onPointerLeave={() => setPressed(false)}
      className={cn(
        "relative flex h-6 w-[42px] shrink-0 items-center rounded-full border p-[2px] outline-none transition-colors duration-200",
        "focus-visible:shadow-[var(--focus-ring)] disabled:opacity-45",
        on ? "justify-end border-volt bg-volt" : "justify-start border-border-strong bg-surface-sunken",
        !label && className,
      )}
    >
      <motion.span
        layout
        transition={{ type: "spring", stiffness: 640, damping: 30 }}
        animate={{ width: pressed ? 22 : 18 }}
        className={cn("block h-[18px] rounded-full", on ? "bg-on-volt" : "bg-ink-muted")}
      />
    </button>
  );

  if (!label) return control;
  return (
    <label className={cn("inline-flex cursor-pointer items-center gap-3 text-sm font-medium text-ink", className)}>
      {control}
      {label}
    </label>
  );
}

```
