Switch
betaAn 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. Add the registry to
components.json, once per app. Without it, the shadcn CLI looks up@txbrendanin a public list that txbrendan/ui does not control.npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.
npx shadcn@latest add @txbrendan/theme3. 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
| Prop | Type | Default | What it does |
|---|---|---|---|
| children | ReactNode | - | Visible label text. Without it, TypeScript requires aria-label, aria-labelledby, or an id. |
| id | string | - | Links a separate label. A label whose htmlFor matches names the switch, and clicking it flips the switch. |
| tone | "neutral" | "brand" | "danger" | "success" | "warning" | "neutral" | Color of the track when it is on. |
| size | "sm" | "md" | "lg" | "md" | Track size and label text size. Density scales them. |
| checked | boolean | - | Controlled state. Use it with onCheckedChange. |
| defaultChecked | boolean | false | Starting state when you do not control it. |
| onCheckedChange | (checked: boolean, details) => void | - | Called with the new state when someone flips the switch. |
| disabled | boolean | false | Blocks flipping and removes the switch from the tab order. The label text dims too. |
| name | string | - | Form field name. The switch sends its value with the form when it is on. |
| aria-invalid | boolean | - | Shows the danger border. Set it when validation fails, and link the message with aria-describedby. |
| className | string | - | Classes for the track. Later classes win, so className='[--switch-h:2rem]' makes it bigger. |
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" />