Skip to content

Switch

Turns a setting on or off, taking effect the moment it's flipped.

SourceBase UIRegistry
import { Field, FieldLabel } from "@/components/ui/field"
import { Switch } from "@/components/ui/switch"

export default function SwitchDemo() {
  return (
    <Field orientation="horizontal" className="w-60">
      <Switch id="auto-deploy" defaultChecked />
      <FieldLabel htmlFor="auto-deploy">Deploy on every push</FieldLabel>
    </Field>
  )
}

Anatomy

  1. 1TrackBrand-filled when on, neutral when off. Its hit area extends invisibly to 44×36px.
  2. 2ThumbSlides across on a spring when toggled, and stretches while pressed.
  3. 3LabelNames the setting, not the action. Clicking it toggles the switch too.

Installation

$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/switch.json

Usage

import { Field, FieldLabel } from "@/components/ui/field"
import { Switch } from "@/components/ui/switch"
<Field orientation="horizontal">
  <Switch id="auto-deploy" />
  <FieldLabel htmlFor="auto-deploy">Deploy on every push</FieldLabel>
</Field>

The thumb travels on a spring and stretches while pressed, the way a physical toggle gives under your finger. With reduced motion it simply snaps into place.

Examples

Switch or checkbox?

A switch is a light switch: flipping it changes something immediately — no Save button, no confirmation. A checkbox is a form answer: nothing happens until the form is submitted. Here the switch restyles the card the instant it changes.

Night shiftStarts at 22:00
Active
"use client"

import * as React from "react"
import { MoonIcon } from "lucide-react"

import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Field, FieldLabel } from "@/components/ui/field"
import { Switch } from "@/components/ui/switch"
import { cn } from "@/lib/utils"

export default function SwitchImmediate() {
  const [dark, setDark] = React.useState(false)

  return (
    <div className="flex w-full max-w-sm flex-col gap-4">
      <Field orientation="horizontal">
        <Switch id="preview-dark" checked={dark} onCheckedChange={setDark} />
        <FieldLabel htmlFor="preview-dark">Dark preview</FieldLabel>
      </Field>
      {/* The switch applies instantly — no Save button, no confirmation. */}
      <div
        className={cn(
          dark ? "dark" : "light",
          "flex items-center justify-between gap-4 rounded-xl border bg-card p-4 text-card-foreground shadow-raised transition-colors duration-base"
        )}
      >
        <div className="flex items-center gap-3">
          <span className="flex size-9 items-center justify-center rounded-lg bg-brand-3 text-brand-11">
            <MoonIcon className="size-4" />
          </span>
          <div className="flex flex-col">
            <span className="text-sm font-medium">Night shift</span>
            <span className="text-xs text-muted-foreground">
              Starts at 22:00
            </span>
          </div>
        </div>
        <div className="flex items-center gap-2">
          <Badge variant="success">Active</Badge>
          <Button size="xs" variant="outline">
            Edit
          </Button>
        </div>
      </div>
    </div>
  )
}
Rule of thumb
If the change needs a Save or Submit button to take effect, use a Checkbox. If people would expect to see the result right away, use a switch.

Settings list

The most common home for switches: one setting per row, the label and a short description on the left, the switch on the right where the eye lands after reading. Separators keep long lists scannable.

import {
  Field,
  FieldContent,
  FieldDescription,
  FieldGroup,
  FieldLabel,
  FieldSeparator,
} from "@/components/ui/field"
import { Switch } from "@/components/ui/switch"

const settings = [
  {
    id: "preview-comments",
    title: "Preview comments",
    description: "Let reviewers leave comments on preview deployments.",
    checked: true,
  },
  {
    id: "skew-protection",
    title: "Skew protection",
    description: "Keep clients on the version they loaded until they refresh.",
    checked: true,
  },
  {
    id: "public-logs",
    title: "Public build logs",
    description: "Anyone with the link can read build output.",
    checked: false,
  },
]

export default function SwitchSettings() {
  return (
    <FieldGroup className="w-full max-w-md gap-4 rounded-xl border bg-card p-5 shadow-raised">
      {settings.map((setting, index) => (
        <div key={setting.id} className="flex flex-col gap-4">
          {index > 0 && <FieldSeparator />}
          <Field orientation="horizontal">
            <FieldContent>
              <FieldLabel htmlFor={setting.id}>{setting.title}</FieldLabel>
              <FieldDescription>{setting.description}</FieldDescription>
            </FieldContent>
            <Switch id={setting.id} defaultChecked={setting.checked} />
          </Field>
        </div>
      ))}
    </FieldGroup>
  )
}

Sizes

size="sm" fits dense tables and compact panels; the default size suits forms and settings pages.

import { Field, FieldLabel } from "@/components/ui/field"
import { Switch } from "@/components/ui/switch"

export default function SwitchSizes() {
  return (
    <div className="flex w-80 flex-col gap-4">
      <Field orientation="horizontal">
        <Switch id="size-sm" size="sm" defaultChecked />
        <FieldLabel htmlFor="size-sm" className="text-body-xs">
          Small — dense lists and table rows
        </FieldLabel>
      </Field>
      <Field orientation="horizontal">
        <Switch id="size-default" defaultChecked />
        <FieldLabel htmlFor="size-default" className="font-normal">
          Default — settings and forms
        </FieldLabel>
      </Field>
    </div>
  )
}

States

Disabled switches keep their position so people can still read the current value. When a setting is required, mark the switch aria-invalid and explain why in a FieldError.

import {
  Field,
  FieldContent,
  FieldError,
  FieldGroup,
  FieldLabel,
} from "@/components/ui/field"
import { Switch } from "@/components/ui/switch"

export default function SwitchStates() {
  return (
    <FieldGroup className="w-80 gap-4">
      <Field orientation="horizontal" data-disabled>
        <Switch id="switch-disabled-off" disabled />
        <FieldLabel htmlFor="switch-disabled-off" className="font-normal">
          Disabled
        </FieldLabel>
      </Field>
      <Field orientation="horizontal" data-disabled>
        <Switch id="switch-disabled-on" disabled defaultChecked />
        <FieldLabel htmlFor="switch-disabled-on" className="font-normal">
          Disabled and on
        </FieldLabel>
      </Field>
      <Field orientation="horizontal" data-invalid>
        <Switch id="switch-invalid" aria-invalid />
        <FieldContent>
          <FieldLabel htmlFor="switch-invalid">
            Two-factor authentication
          </FieldLabel>
          <FieldError>
            Your organization requires 2FA for all members.
          </FieldError>
        </FieldContent>
      </Field>
    </FieldGroup>
  )
}

Guidelines

When to use

  • For settings that apply immediately: notifications, integrations, feature toggles, dark mode.
  • For binary states people understand without extra explanation — on or off.

When not to use

Labels

Label the setting, not the state. The switch already shows on and off, so the label should name what is being turned on — never Enable / Disable, and never text that changes when the switch flips.

Email notifications

DoThe label names the setting; the switch shows its state.

Notifications are on — click to turn off

Don’tA label that flips with the value makes the state impossible to read.

Feedback

Because a switch acts immediately, confirm the result when it isn't visible on screen — a Toast such as Preview comments enabled — and undo the change (with an explanation) if the request fails.

Accessibility

The switch renders role="switch" with aria-checked, plus a hidden checkbox input for forms. A sibling FieldLabel with htmlFor both names it and toggles it on click.

KeyBehavior
Tab
Moves focus to the switch. Focus shows a 2px ring offset from the track.
SpaceEnter
Toggles the switch.
  • Boundaries. The off track uses the input token, which meets 3:1 against the page (WCAG 1.4.11); the on track uses the brand solid. The thumb's position — not color alone — carries the state.
  • Target size. The 36×20px track has a transparent hit area extending it to 44×36px.
  • Motion. Under prefers-reduced-motion the thumb changes position and width instantly, without the spring.

API reference

Switch

Renders a <span role="switch"> with a thumb and a hidden <input type="checkbox">. Accepts every prop of Base UI's Switch.Root.

PropTypeDefault
size

Track size: 28×16px or 36×20px.

"sm" | "default""default"
checked

Whether the switch is on. Use with onCheckedChange when controlled.

booleanNo default
defaultChecked

The initial state when uncontrolled.

booleanfalse
onCheckedChange

Called when the switch is toggled — apply the setting here.

(checked: boolean, details) => voidNo default
name

Submits the switch with a form.

stringNo default
value

The submitted value when on.

stringNo default
uncheckedValue

The submitted value when off.

stringNo default
required

Requires the switch to be on before submitting.

booleanfalse
disabled

Ignores input and fades the control.

booleanfalse
readOnly

Shows the state but prevents changing it.

booleanfalse
aria-invalid

Shows the error state on the off track.

booleanNo default