Skip to content
Monogem

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

switch-showcase.tsx
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":

LayoutWhen
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 / compactLabel 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

StateLook
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)
DisabledReduced 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

CheckboxSwitch
MeaningSelection, confirmation, or form valueLive system state
EffectUsually applied on submitApplied immediately
Count in a groupZero, one, or manyAlways standalone
Default label positionRight of controlLeft 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.

TokenWhere used
--primaryOn-track fill
--inputOff-track fill
--backgroundThumb
--ringFocus-visible ring
--destructiveError text
--muted-foregroundHelper text
--radius-fullTrack and thumb shape
--scale-10Track width (40px)
--scale-5Track height (20px)
--scale-4Thumb diameter (16px)
--scale-0-5Thumb inset from the track edge
--scale-2Track-to-label gap
--scale-1-5Control-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 checked state 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-checked on this element — it's unnecessary here and duplicates the native checked state the browser already exposes, the same redundancy Checkbox avoids between indeterminate and aria-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 checked state carry the meaning.
  • Native disabled removes 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.