Skip to content
txbrendan/ui

Switch

beta

An on and off control for a setting that takes effect right away, with a built-in label, tones, and sizes.

Preview

tone

size

states

settings

no text

Install

  1. 1. Add the registry to components.json, once per app. Without it, the shadcn CLI looks up @txbrendan in a public list that txbrendan/ui does not control.

    npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"
  2. 2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.

    npx shadcn@latest add @txbrendan/theme
  3. 3. Add the component.

    npx shadcn@latest add @txbrendan/switch

Use it for

  • Turning a setting on or off when the change applies at once, like email alerts or auto-save.
  • Pass the label text as children. Name the setting, like 'Email alerts', not the action, like 'Turn on alerts'.
  • A settings row with the text first: give the switch an id and point a label's htmlFor at it.
  • Use tone to color the track when it is on. Keep neutral unless 'on' needs to stand out.

Avoid it for

  • A choice that only applies after the person presses Save. Use a checkbox.
  • Picking options from a list. Use checkboxes or a radio group.
  • An id with no label pointing at it. TypeScript accepts the id, but the switch then has no name.

Props

  • children

    Visible label text. Without it, TypeScript requires aria-label, aria-labelledby, or an id.

    ReactNode

  • id

    Links a separate label. A label whose htmlFor matches names the switch, and clicking it flips the switch.

    string

  • tone

    Color of the track when it is on.

    "neutral" | "brand" | "danger" | "success" | "warning" · default "neutral"

  • size

    Track size and label text size. Density scales them.

    "sm" | "md" | "lg" · default "md"

  • checked

    Controlled state. Use it with onCheckedChange.

    boolean

  • defaultChecked

    Starting state when you do not control it.

    boolean · default false

  • onCheckedChange

    Called with the new state when someone flips the switch.

    (checked: boolean, details) => void

  • disabled

    Blocks flipping and removes the switch from the tab order. The label text dims too.

    boolean · default false

  • name

    Form field name. The switch sends its value with the form when it is on.

    string

  • aria-invalid

    Shows the danger border. Set it when validation fails, and link the message with aria-describedby.

    boolean

  • className

    Classes for the track. Later classes win, so className='[--switch-h:2rem]' makes it bigger.

    string

Accessibility

  • Renders a span with role="switch" and aria-checked plus a hidden native checkbox, so the value is sent with the form.
  • Space and Enter both flip it.
  • TypeScript requires visible children, aria-label, aria-labelledby, or an id. It cannot check that a label points at the id.
  • In server HTML, only aria-label and aria-labelledby name the switch. A label from children or an id is linked once JavaScript runs.
  • Clicking the label text also flips it, so the target is bigger than the track.
  • The off track uses --input, which the theme tests hold to 3:1 against the page, so the off state stays visible.
  • The thumb on the on track uses the tone's foreground, which the theme tests hold to 4.5:1 against the tone.
  • The focus ring uses --ring, which the theme tests hold to 3:1 against the page and cards.
  • In forced colors the thumb fills with the system text color, and an on track fills with the system highlight, so on and off still look different.
  • The thumb slides with --motion-* tokens. Reduced motion keeps the color change and drops the slide.

Examples

With a label

<Switch name="alerts" defaultChecked>
  Email alerts
</Switch>

Controlled

"use client";
import { useState } from "react";

function AutoSave() {
  const [on, setOn] = useState(true);
  return (
    <Switch checked={on} onCheckedChange={setOn} tone="brand">
      Auto-save drafts
    </Switch>
  );
}

Settings row with the text first

<div className="flex items-center justify-between gap-4">
  <label htmlFor="sync" className="text-sm">
    Sync over mobile data
  </label>
  <Switch id="sync" />
</div>

No visible text, in a toolbar

<Switch size="sm" aria-label="Show weekends" />