# Switch

> Toggle switch in three sizes with optional label and description following the ARIA switch pattern.

- Author: Atelier UI
- License: MIT
- Page: https://chromotion.dev/en/components/switch
- Site: Atelier UI (https://chromotion.dev)

## Install

```bash
npx shadcn@latest add https://chromotion.dev/r/switch.json
```

Stack: React + TypeScript + Tailwind CSS v4, shadcn/ui conventions. `cn` comes from `@/lib/utils`.

## Dependencies

No extra dependencies.

## Usage

```tsx title="components/switch-demo.tsx"
"use client"

import * as React from "react"

import { Switch } from "@/components/switch"

export default function SwitchDemo() {
  const [airplane, setAirplane] = React.useState(false)
  return (
    <div className="grid w-full max-w-sm gap-6">
      <div className="flex items-center gap-4">
        <Switch size="sm" aria-label="Small" defaultChecked />
        <Switch size="md" aria-label="Medium" defaultChecked />
        <Switch size="lg" aria-label="Large" defaultChecked />
        <Switch aria-label="Disabled" disabled />
      </div>
      <Switch
        label="Airplane mode"
        description={airplane ? "Radios are off." : "Wi-Fi and Bluetooth stay on."}
        checked={airplane}
        onCheckedChange={setAirplane}
      />
      <Switch label="Marketing emails" description="Product news, once a month at most." defaultChecked />
    </div>
  )
}
```

## Source

```tsx title="components/switch.tsx"
"use client"

import * as React from "react"

import { cn } from "@/lib/utils"

const sizes = {
  sm: { track: "h-5 w-9", thumb: "size-4", shift: "translate-x-4" },
  md: { track: "h-6 w-11", thumb: "size-5", shift: "translate-x-5" },
  lg: { track: "h-8 w-14", thumb: "size-7", shift: "translate-x-6" },
} as const

export interface SwitchProps extends Omit<React.ComponentProps<"button">, "onChange" | "role"> {
  checked?: boolean
  defaultChecked?: boolean
  onCheckedChange?: (checked: boolean) => void
  size?: keyof typeof sizes
  /** Visible label rendered next to the switch */
  label?: React.ReactNode
  description?: React.ReactNode
}

export function Switch({
  checked,
  defaultChecked = false,
  onCheckedChange,
  size = "md",
  label,
  description,
  disabled,
  className,
  onClick,
  id,
  ...props
}: SwitchProps) {
  const [internal, setInternal] = React.useState(defaultChecked)
  const generated = React.useId()
  const switchId = id ?? generated
  const on = checked ?? internal
  const s = sizes[size]

  const control = (
    <button
      {...props}
      id={switchId}
      type="button"
      role="switch"
      aria-checked={on}
      disabled={disabled}
      onClick={(event) => {
        onClick?.(event)
        if (event.defaultPrevented) return
        setInternal(!on)
        onCheckedChange?.(!on)
      }}
      className={cn(
        "relative inline-flex shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent outline-none transition-colors focus-visible:ring-4 focus-visible:ring-ring/40 disabled:cursor-not-allowed disabled:opacity-50",
        s.track,
        on ? "bg-primary" : "bg-input",
        className,
      )}
    >
      <span
        aria-hidden
        className={cn(
          "pointer-events-none block rounded-full bg-background shadow-sm ring-0 transition-transform duration-200",
          s.thumb,
          on ? s.shift : "translate-x-0",
        )}
      />
    </button>
  )

  if (!label) return control
  return (
    <div className="flex items-start gap-3">
      {control}
      <label htmlFor={switchId} className={cn("grid cursor-pointer gap-0.5 text-sm leading-tight", disabled && "opacity-50")}>
        <span className="font-medium">{label}</span>
        {description && <span className="text-muted-foreground">{description}</span>}
      </label>
    </div>
  )
}
```
