Component · Core
Switch
Overview
An immediate on/off control for a live setting — flipping it takes effect right away, unlike Checkbox, which represents a selection or form value that's usually applied on submit. See Switch vs. Checkbox below for the full distinction.
Anatomy
Track (--scale-10 × --scale-5, --radius-full) → thumb (--scale-4 circle, --radius-full,
inset --scale-0-5) → label.
- Track — the pill-shaped rail.
- Thumb — the circle that moves between off and on.
- Label — the setting’s name.
Live Example
Settings row (default)
Settings row + helper text
Sent every Monday at 9am in your timezone.
Standalone / compact
Code Example
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
/**
* Live Switch example — a native `<input type="checkbox" role="switch">`. The
* default settings-row layout puts the label on the left and the Switch
* right-aligned (the row reads: what it does → its current state); the
* standalone layout puts the label beside the control, same side as Checkbox.
* Both keep the same activation and accessibility rules. Any helper / error
* text aligns beneath the start of the label text.
*
* Switch keeps a component-specific `--destructive` error-text treatment
* (system-wide alignment with Input's `--foreground` contract is deferred).
*/
export function SwitchShowcase() {
return (
<div className="flex flex-col gap-8">
<section className="flex flex-col gap-3">
<h3 className="text-sm font-medium leading-snug text-muted-foreground">
Settings row (default)
</h3>
<div className="flex max-w-sm flex-col gap-4">
<div className="flex items-center justify-between gap-4">
<Label htmlFor="sw-2fa">Require two-factor sign-in</Label>
<Switch id="sw-2fa" defaultChecked />
</div>
<div className="flex items-center justify-between gap-4">
<Label htmlFor="sw-beta">Show beta features</Label>
<Switch id="sw-beta" />
</div>
<div className="flex items-center justify-between gap-4">
<Label htmlFor="sw-disabled" className="opacity-50">
Delete account (contact support)
</Label>
<Switch id="sw-disabled" disabled />
</div>
</div>
</section>
<section className="flex flex-col gap-3">
<h3 className="text-sm font-medium leading-snug text-muted-foreground">
Settings row + helper text
</h3>
<div className="flex max-w-sm flex-col gap-1.5">
<div className="flex items-center justify-between gap-4">
<Label htmlFor="sw-digest">Weekly summary email</Label>
<Switch
id="sw-digest"
defaultChecked
aria-describedby="sw-digest-helper"
/>
</div>
<p
id="sw-digest-helper"
className="text-xs leading-snug text-muted-foreground"
>
Sent every Monday at 9am in your timezone.
</p>
</div>
</section>
<section className="flex flex-col gap-3">
<h3 className="text-sm font-medium leading-snug text-muted-foreground">
Standalone / compact
</h3>
<div className="flex items-center gap-2">
<Switch id="sw-standalone" />
<Label htmlFor="sw-standalone">Airplane mode</Label>
</div>
</section>
</div>
);
}Variants
Label placement
Two documented layouts — neither is "the only valid one":
| Layout | When |
|---|---|
| Settings-row (default) | Label on the left, Switch right-aligned — the standard settings-list pattern (row reads left to right: what it does → its current state). |
| Standalone / compact | Label beside the control, same side as Checkbox/Radio, when a Switch appears on its own rather than in a settings-row list and the row layout doesn't fit. |
Both keep the same activation and accessibility rules below — only the visual position of the label changes.
States
| State | Look |
|---|---|
| Off | --input track fill, thumb at the left inset |
| On | --primary track fill, thumb at the right inset |
| Focus-visible | --ring around the track on :focus-visible (keyboard focus only), flush against the track edge, tracing --radius-full — the field focus-ring geometry (Input, Color) |
| Disabled | Reduced opacity (opacity-50; Color — Disabled dimming), native disabled attribute, no pointer events |
No dedicated hover state — the same reasoning Input uses: the whole track is already an obvious click target, and a hover shift would add noise without adding clarity.
Read-only state
Not part of the baseline. Disabled presentation already covers "this can't be changed right now,"
and native switch semantics don't define a distinct read-only behavior the way readonly does
for Input. Not inventing one here.
Usage Guidance
Switch vs. Checkbox
| Checkbox | Switch | |
|---|---|---|
| Meaning | Selection, confirmation, or form value | Live system state |
| Effect | Usually applied on submit | Applied immediately |
| Count in a group | Zero, one, or many | Always standalone |
| Default label position | Right of control | Left of control (settings-row) |
Don't reach for Switch just because it reads as more modern — a plain multi-select list is still a Checkbox group, regardless of how it looks.
Status text
Optional guidance, not baseline. The thumb's position and the track's fill already communicate on/off — a redundant "On"/"Off" caption next to the Switch can be added when the setting's effect isn't otherwise obvious from context, but it's not a required part of the anatomy.
Activation behavior
- Click or tap anywhere on the track (or the label, in either layout) toggles the Switch immediately.
- Space toggles the native checkbox-based Switch when it has focus — same key as Checkbox.
- Enter is not required to toggle it, for the native checkbox-based baseline — this avoids accidental double-activation in a form that also submits on Enter. This is scoped to the native baseline, not a universal claim about every possible Switch implementation.
Supporting text
A Switch can carry helper or error text below it, both in Input's
Caption typography (same for helper and error — only
the color differs): --muted-foreground for helper, matching
Input's shared form-field model; --destructive for error
(e.g. "Couldn't disable — an active subscription depends on this"). Switch currently retains this
component-specific --destructive error-text color; it does not yet match Input's canonical
--foreground error-message contract. That color difference may be reconciled in a future
release; the typography is already aligned. Supporting text is less common
than on Checkbox or Radio Group, since most Switches are self-explanatory, but the pattern is
available when a setting's effect needs a line of context.
Helper or error text aligns beneath the start of the label text, not beneath the track — consistent with Checkbox. In the settings-row layout, the label already starts at the row's left edge, so the message naturally lines up underneath it.
Same association rule as the rest of the shared form-field model:
helper text and error text each get a stable id, and the Switch control references whichever
message is currently visible via aria-describedby. Error text replaces helper text rather
than appending to it — the same default Input uses. aria-invalid="true" is added on the Switch
only while it's actually invalid, and removed once corrected.
Tokens
Switch uses existing Monogem semantic and primitive tokens to stay consistent with the design system; it introduces no component-specific styling values.
| Token | Where used |
|---|---|
--primary | On-track fill |
--input | Off-track fill |
--background | Thumb |
--ring | Focus-visible ring |
--destructive | Error text |
--muted-foreground | Helper text |
--radius-full | Track and thumb shape |
--scale-10 | Track width (40px) |
--scale-5 | Track height (20px) |
--scale-4 | Thumb diameter (16px) |
--scale-0-5 | Thumb inset from the track edge |
--scale-2 | Track-to-label gap |
--scale-1-5 | Control-to-message gap, same value as Input |
Do / Don’t
Do
- Default to the settings-row layout (label left, Switch right) in list-like settings screens.
- Use the standalone layout when a Switch appears on its own, outside a settings row.
- Apply a setting immediately when the Switch is flipped — that's the point of choosing Switch over Checkbox.
Don’t
- Use Switch for a value that's collected now and applied later — that's Checkbox.
- Use Switch for a choice among several mutually exclusive options — that's Radio Group.
- Invent a read-only visual state — disabled already covers "can't change this."
- Rely on track color alone to communicate state — the thumb's position is the primary signal.
Accessibility
Native implementation — the only baseline implementation:
<input type="checkbox" role="switch">
- The native
checkedstate already communicates on/off to assistive tech;role="switch"changes only how the control is announced (as a switch, not a checkbox). - Don't also set
aria-checkedon this element — it's unnecessary here and duplicates the nativecheckedstate the browser already exposes, the same redundancy Checkbox avoids betweenindeterminateandaria-checked="mixed". - Native label association (
htmlFor/id— see Label), focusability, and Space-key activation all come from the element for free, same as Checkbox. - Track color is never the only signal — thumb position plus the native
checkedstate carry the meaning. - Native
disabledremoves the control from the tab order automatically.
Custom, non-native Switch implementations are outside Monogem's current baseline. A Switch
built on a non-native element (e.g. a <div> or <button> standing in for the control) needs a
complete ARIA interaction pattern — the switch role, accessible naming, focusability, state
management, and keyboard behavior all have to be built and kept in sync by hand. Adding
aria-checked alone is not sufficient for that case and isn't documented here; the native
checkbox-based implementation above is Monogem's only supported baseline.
Related Components / Patterns
- Checkbox — a value submitted with a form.
- Radio Group — exactly one choice from a visible list.