opsinjs
ComponentsActions and forms

Switch

A labelled on-or-off control that commits the moment it is flipped, for a single setting rather than a form value submitted later.

Preview

Theme
Density
Text
Status
switch · base base · style base-lyraOpen under the product theme

Installation

pnpm dlx shadcn@latest add @opsinjs/switch

The @opsinjs namespace is declared in your components.json. Everything it installs is code you then own. There is no runtime package to keep in step.

Usage

import { Switch } from "@/components/ui/switch"
<Switch
  label="Larger text"
  checked={largerText}
  onCheckedChange={setLargerText}
/>

When to use it

Use it when

  • A single setting, such as a Larger text preference, that takes effect the moment it is flipped.
  • A binary state the product commits straight away, with nothing submitted later and the reader free to flip it back.

Do not use it when

  • The toggle would record that the reader agreed to something. Consent is a decision with a record.Use consent-sheet instead.
  • The value is proposed now and submitted later with a form.Use checkbox instead.

Anatomy

  1. SwitchThe root, a label element floored at the 44pt target. The whole row is pressable.Controlled by label
  2. Switch.LabelThe visible words and the accessible name, applied to the control through aria-labelledby.Controlled by label
  3. Switch.DescriptionThe optional helper line, read as the control's description through aria-describedby.Controlled by description
  4. Switch.ControlThe track, a Base UI Switch.Root with role="switch" and aria-checked. Muted when off, primary fill when on.Controlled by checked
  5. Switch.ThumbThe disc that slides: left for off, right for on, so position carries the state too.
  • Switch 1data-slot="switch", the whole target
    • Switch.Label 1data-slot="switch-label"
    • Switch.Description 0..1data-slot="switch-description", only when description is set
    • Switch.Control 1data-slot="switch-control", role="switch"
      • Switch.Thumb 1data-slot="switch-thumb"

Examples

A setting that applies now

checked and onCheckedChange commit "Larger text" on the flip, not on a later submit. No helper line, because the label alone says what the setting does.

Theme
Density
Text
Status
switch-a-setting-that-applies-now · base base · style base-lyraOpen under the product theme

A setting with a helper line

description adds one sentence under the label saying what turning the switch on changes. It is read as the description, so the accessible name stays the label.

Theme
Density
Text
Status
switch-with-a-description · base base · style base-lyraOpen under the product theme

Content guidelines

Name the setting the switch turns on, not its state: Larger text, never On or Turn on larger text. Sentence case, a few words, no full stop. The helper line says what the setting does; it never repeats the label or carries a reading, a threshold or a unit.

Do

"Larger text" names the setting and stays true whichever way the switch is flipped.

Don’t

"I agree" records consent, which belongs in a ConsentSheet.

Accessibility

Audited against WCAG 2.2 AA. A source pass over the full rubric and a rendered pass with axe, both author-run rather than an independent review, with clinical review still pending. The audit found nothing to change in this component's source.

What the audit confirmed.

  • role="switch" comes from Base UI's Switch.Root, the accessible name is a visible label span tied by aria-labelledby, and state rides aria-checked.
  • On and off are carried by thumb position and aria-checked, not the fill alone, and a border-border hairline survives forced-colors: active and a stripped fill.
  • The 24px AA target floor is met by the track, and the 44pt house floor by the enclosing label row, which toggles the control.
  • Focus sits on the track with the house ring, and both transitions collapse under prefers-reduced-motion: reduce through the fast-duration token.
  • pnpm run check:a11y runs on every commit: only neutral chrome and the bridged primary fill, so no colour literal, no type size in px, and no banned word.

Still unmeasured. No contrast pair has been read yet, so the report below stands in for numbers nobody has produced.

KeyActionNotes
TabMoves focus onto the switchA single tab stop. A disabled switch is skipped.
Shift+TabMoves focus back off the switchOne stop in reverse too.
SpaceFlips the switchonCheckedChange fires with the new boolean.
EnterFlips the switchBase UI binds Enter as well as Space.
Arrow keysNothingNothing to move focus between.
PairThemeAPCA LcWCAG 2.2Floor
body text on the pagelight101.617.18:1Pass
body text on the pagedark-100.518.00:1Pass
body text on a cardlight104.717.96:1Pass
body text on a carddark-99.616.32:1Pass
body text on the muted groundlight98.116.32:1Pass
body text on the muted grounddark-97.513.77:1Pass
secondary text on the pagelight83.97.42:1Pass
secondary text on the pagedark-80.713.44:1Pass
secondary text on a cardlight87.07.76:1Pass
secondary text on a carddark-79.812.19:1Pass
secondary text on the muted groundlight80.47.05:1Pass
secondary text on the muted grounddark-77.810.28:1Pass
a hairline boundary on the pagelight19.31.41:1Below floor
a hairline boundary on the pagedark-8.21.90:1Below floor
a hairline boundary on a cardlight22.41.47:1Below floor
a hairline boundary on a carddark-7.31.72:1Below floor
a hairline boundary on the muted groundlight15.81.34:1Below floor
a hairline boundary on the muted grounddark0.01.45:1Below floor
a placeholder boundary on the pagelight69.84.52:1Pass
a placeholder boundary on the pagedark-51.07.62:1Pass
a placeholder boundary on a cardlight72.84.72:1Pass
a placeholder boundary on a carddark-50.16.91:1Pass
a placeholder boundary on the muted groundlight66.24.29:1Pass
a placeholder boundary on the muted grounddark-48.15.83:1Pass
a placeholder fill on the pagelight8.31.18:1Below floor
a placeholder fill on the pagedark-8.21.90:1Below floor
a placeholder fill on a cardlight11.41.23:1Below floor
a placeholder fill on a carddark-7.31.72:1Below floor
a placeholder fill on the muted groundlight0.01.12:1Below floor
a placeholder fill on the muted grounddark0.01.45:1Below floor
a placeholder boundary on its own filllight58.93.84:1Pass
a placeholder boundary on its own filldark-41.54.01:1Below floor
the card hairline on the pagelight69.84.52:1Pass
the card hairline on the pagedark-28.74.19:1Below floor
the card hairline on a cardlight72.84.72:1Pass
the card hairline on a carddark-27.93.80:1Below floor

These are the measured token pairs this component draws colour from, not a measurement of the component itself.

Data attributes

AttributeConditionValue
data-slotOn the root and every inner partswitch, switch-label, switch-description, switch-control, switch-thumb
data-checked / data-uncheckedOn the control and the thumb, one or the otherPresent with no value; reflects checked
data-disabledOn the control while disabledPresent with no value

API reference

Prop

Type

Generated from SwitchProps in registry/bases/base/switch.tsx.

checked and onCheckedChange make this a controlled component with no internal state. label is required: a missing or empty label raises a development warning. disabled keeps the switch visible and labelled while refusing focus and every key. className is merged onto the root row and must carry neither colour axis.

  • Checkbox proposes a value a form submits later. A switch commits on the flip.
  • ConsentSheet records a decision with a who and a when. A switch keeps no record.
  • SegmentedControl chooses one of several options. A switch is the two-state case.

On this page