---
title: Switch
description: An immediate on-or-off control for settings.
group: Components
order: 64
---

Use a switch for settings that apply as soon as they are turned on or off.

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

export function Example() {
  return (
    <label className="flex items-center gap-2.5 text-sm tracking-wide">
      Airplane mode
      <Switch id="airplane" />
    </label>
  )
}
```

## Installation

```bash
npx @arctis-sh/@arctis-sh/ui@latest add switch
```

## Usage

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

```tsx
<Switch />
```

```tsx
<label className="flex items-center gap-2.5 text-sm tracking-wide">
  Airplane mode
  <Switch id="airplane" />
</label>
```

## Description

Pair the control with a title and short helper text, aligned on the trailing edge.

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

export function Example() {
  return (
    <label className="flex w-full items-start justify-between gap-4">
      <span className="grid gap-1">
        <span className="text-sm font-medium tracking-wide">
          Watch theme tokens
        </span>
        <span className="text-sm text-muted-foreground">
          Rebuild docs when CSS variables change under globals.css.
        </span>
      </span>
      <Switch id="watch-tokens" className="mt-0.5" />
    </label>
  )
}
```

## Choice card

Wrap the row in a `Card` to make the full surface clickable. The unchecked card stays outlined; the checked card fills.

```tsx
import * as React from "react"
import { Card, CardContent } from "@/components/ui/card"
import { Switch } from "@/components/ui/switch"
import { cn } from "@/lib/utils"

export function Example() {
  const [docs, setDocs] = React.useState(true)
  const [registry, setRegistry] = React.useState(false)

  return (
    <div className="grid w-full gap-3">
      <label className="block w-full cursor-pointer">
        <Card className={cn("py-0", !docs && "bg-transparent")}>
          <CardContent className="flex items-start justify-between gap-4 py-3">
            <span className="grid gap-1">
              <span className="text-sm font-medium tracking-wide">
                Include in docs build
              </span>
              <span className="text-sm text-muted-foreground">
                Ships markdown, demos, and tokens with the registry export.
              </span>
            </span>
            <Switch
              id="card-docs"
              checked={docs}
              onCheckedChange={setDocs}
              className="mt-0.5"
            />
          </CardContent>
        </Card>
      </label>
      <label className="block w-full cursor-pointer">
        <Card className={cn("py-0", !registry && "bg-transparent")}>
          <CardContent className="flex items-start justify-between gap-4 py-3">
            <span className="grid gap-1">
              <span className="text-sm font-medium tracking-wide">
                Sync local registry
              </span>
              <span className="text-sm text-muted-foreground">
                Pull component updates when the registry changes.
              </span>
            </span>
            <Switch
              id="card-registry"
              checked={registry}
              onCheckedChange={setRegistry}
              className="mt-0.5"
            />
          </CardContent>
        </Card>
      </label>
    </div>
  )
}
```

## Disabled

Set `disabled` to lock the control. Dim the label too so the whole row reads as inactive.

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

export function Example() {
  return (
    <label className="flex items-center gap-2.5 text-sm tracking-wide opacity-60">
      Publish package — unlock after review
      <Switch id="publish-locked" disabled />
    </label>
  )
}
```

## Invalid

Mark the control with `aria-invalid` when validation fails, and use destructive text for the label.

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

export function Example() {
  return (
    <label className="flex items-center gap-2.5 text-sm font-medium tracking-wide text-destructive">
      Accept the license
      <Switch id="license" aria-invalid />
    </label>
  )
}
```

## Size

Use `size="sm"` for a tighter track. Default is the standard size.

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

export function Example() {
  return (
    <div className="flex w-44 flex-col gap-4">
      <label className="flex w-full items-center justify-between gap-4 text-sm tracking-wide">
        Small
        <Switch id="size-sm" size="sm" defaultChecked />
      </label>
      <label className="flex w-full items-center justify-between gap-4 text-sm tracking-wide">
        Default
        <Switch id="size-default" defaultChecked />
      </label>
    </div>
  )
}
```

## Radius

Use `radius` for square (`none`), standard (`default`), or pill-shaped (`full`) corners.

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

export function Example() {
  return (
    <div className="flex w-52 flex-col gap-4">
      <label className="flex w-full items-center justify-between gap-4 text-sm tracking-wide">
        None
        <Switch id="radius-none" radius="none" defaultChecked />
      </label>
      <label className="flex w-full items-center justify-between gap-4 text-sm tracking-wide">
        Usual corners
        <Switch id="radius-default" defaultChecked />
      </label>
      <label className="flex w-full items-center justify-between gap-4 text-sm tracking-wide">
        Full
        <Switch id="radius-full" radius="full" defaultChecked />
      </label>
    </div>
  )
}
```

## API Reference

### Switch

| Prop | Type | Default |
| --- | --- | --- |
| checked | boolean | — |
| defaultChecked | boolean | false |
| onCheckedChange | (checked: boolean) => void | — |
| size | "default" \| "sm" | "default" |
| radius | "none" \| "default" \| "full" | "default" |
| disabled | boolean | false |
| aria-invalid | boolean | — |
| id | string | — |
| className | string | — |

Also accepts the other native `button` attributes.

```tsx
<Switch checked={checked} onCheckedChange={setChecked} />
```

## Source

```tsx
"use client";

import {
  useId,
  useState,
  type ButtonHTMLAttributes,
  type MouseEvent,
} from "react";
import { cn } from "@/lib/utils";

type SwitchSize = "default" | "sm";
type SwitchRadius = "none" | "default" | "full";

type SwitchProps = Omit<
  ButtonHTMLAttributes<HTMLButtonElement>,
  "onChange" | "value" | "children"
> & {
  checked?: boolean;
  defaultChecked?: boolean;
  onCheckedChange?: (checked: boolean) => void;
  size?: SwitchSize;
  radius?: SwitchRadius;
};

const trackRadiusClass: Record<SwitchRadius, string> = {
  none: "rounded-none",
  default: "rounded-md",
  full: "rounded-full",
};

const thumbRadiusClass: Record<SwitchRadius, string> = {
  none: "rounded-none",
  default: "rounded-[calc(var(--radius-md)-0.125rem)]",
  full: "rounded-full",
};

export function Switch({
  checked,
  defaultChecked = false,
  onCheckedChange,
  size = "default",
  radius = "default",
  disabled,
  className,
  id,
  onClick,
  ...props
}: SwitchProps) {
  const generatedId = useId();
  const inputId = id ?? generatedId;
  const controlled = checked !== undefined;
  const [uncontrolled, setUncontrolled] = useState(defaultChecked);
  const isChecked = controlled ? checked : uncontrolled;

  function toggle() {
    if (disabled) return;
    const next = !isChecked;
    if (!controlled) setUncontrolled(next);
    onCheckedChange?.(next);
  }

  function handleClick(event: MouseEvent<HTMLButtonElement>) {
    onClick?.(event);
    if (!event.defaultPrevented) toggle();
  }

  return (
    <button
      type="button"
      {...props}
      role="switch"
      id={inputId}
      data-slot="switch"
      data-size={size}
      data-radius={radius}
      data-state={isChecked ? "checked" : "unchecked"}
      aria-checked={isChecked}
      disabled={disabled}
      className={cn(
        "group/switch peer inline-flex shrink-0 items-center border border-transparent p-0.5 transition-colors duration-200 ease-out",
        "disabled:pointer-events-none disabled:opacity-40",
        "data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
        "aria-invalid:border-destructive aria-invalid:bg-destructive/5",
        trackRadiusClass[radius],
        size === "default" && "h-5 w-9",
        size === "sm" && "h-4 w-7",
        className,
      )}
      onClick={handleClick}
    >
      <span
        data-slot="switch-thumb"
        className={cn(
          "pointer-events-none block bg-background transition-transform duration-200 ease-out group-data-[state=checked]/switch:bg-primary-foreground",
          thumbRadiusClass[radius],
          size === "default" &&
            "size-3.5 translate-x-0 group-data-[state=checked]/switch:translate-x-4",
          size === "sm" &&
            "size-2.5 translate-x-0 group-data-[state=checked]/switch:translate-x-3",
        )}
      />
    </button>
  );
}
```
