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
switch · base base · style base-lyraOpen under the product themeInstallation
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-sheetinstead. - The value is proposed now and submitted later with a form.Use
checkboxinstead.
Anatomy
SwitchThe root, a label element floored at the 44pt target. The whole row is pressable.Controlled bylabelSwitch.LabelThe visible words and the accessible name, applied to the control through aria-labelledby.Controlled bylabelSwitch.DescriptionThe optional helper line, read as the control's description through aria-describedby.Controlled bydescriptionSwitch.ControlThe track, a Base UI Switch.Root with role="switch" and aria-checked. Muted when off, primary fill when on.Controlled bycheckedSwitch.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.
switch-a-setting-that-applies-now · base base · style base-lyraOpen under the product themeA 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.
switch-with-a-description · base base · style base-lyraOpen under the product themeContent 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 byaria-labelledby, and state ridesaria-checked.- On and off are carried by thumb position and
aria-checked, not the fill alone, and aborder-borderhairline survivesforced-colors: activeand 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: reducethrough the fast-duration token. pnpm run check:a11yruns on every commit: only neutral chrome and the bridged primary fill, so no colour literal, no type size inpx, and no banned word.
Still unmeasured. No contrast pair has been read yet, so the report below stands in for numbers nobody has produced.
| Key | Action | Notes |
|---|---|---|
| Tab | Moves focus onto the switch | A single tab stop. A disabled switch is skipped. |
| Shift+Tab | Moves focus back off the switch | One stop in reverse too. |
| Space | Flips the switch | onCheckedChange fires with the new boolean. |
| Enter | Flips the switch | Base UI binds Enter as well as Space. |
| Arrow keys | Nothing | Nothing to move focus between. |
| Pair | Theme | APCA Lc | WCAG 2.2 | Floor |
|---|---|---|---|---|
| body text on the page | light | 101.6 | 17.18:1 | Pass |
| body text on the page | dark | -100.5 | 18.00:1 | Pass |
| body text on a card | light | 104.7 | 17.96:1 | Pass |
| body text on a card | dark | -99.6 | 16.32:1 | Pass |
| body text on the muted ground | light | 98.1 | 16.32:1 | Pass |
| body text on the muted ground | dark | -97.5 | 13.77:1 | Pass |
| secondary text on the page | light | 83.9 | 7.42:1 | Pass |
| secondary text on the page | dark | -80.7 | 13.44:1 | Pass |
| secondary text on a card | light | 87.0 | 7.76:1 | Pass |
| secondary text on a card | dark | -79.8 | 12.19:1 | Pass |
| secondary text on the muted ground | light | 80.4 | 7.05:1 | Pass |
| secondary text on the muted ground | dark | -77.8 | 10.28:1 | Pass |
| a hairline boundary on the page | light | 19.3 | 1.41:1 | Below floor |
| a hairline boundary on the page | dark | -8.2 | 1.90:1 | Below floor |
| a hairline boundary on a card | light | 22.4 | 1.47:1 | Below floor |
| a hairline boundary on a card | dark | -7.3 | 1.72:1 | Below floor |
| a hairline boundary on the muted ground | light | 15.8 | 1.34:1 | Below floor |
| a hairline boundary on the muted ground | dark | 0.0 | 1.45:1 | Below floor |
| a placeholder boundary on the page | light | 69.8 | 4.52:1 | Pass |
| a placeholder boundary on the page | dark | -51.0 | 7.62:1 | Pass |
| a placeholder boundary on a card | light | 72.8 | 4.72:1 | Pass |
| a placeholder boundary on a card | dark | -50.1 | 6.91:1 | Pass |
| a placeholder boundary on the muted ground | light | 66.2 | 4.29:1 | Pass |
| a placeholder boundary on the muted ground | dark | -48.1 | 5.83:1 | Pass |
| a placeholder fill on the page | light | 8.3 | 1.18:1 | Below floor |
| a placeholder fill on the page | dark | -8.2 | 1.90:1 | Below floor |
| a placeholder fill on a card | light | 11.4 | 1.23:1 | Below floor |
| a placeholder fill on a card | dark | -7.3 | 1.72:1 | Below floor |
| a placeholder fill on the muted ground | light | 0.0 | 1.12:1 | Below floor |
| a placeholder fill on the muted ground | dark | 0.0 | 1.45:1 | Below floor |
| a placeholder boundary on its own fill | light | 58.9 | 3.84:1 | Pass |
| a placeholder boundary on its own fill | dark | -41.5 | 4.01:1 | Below floor |
| the card hairline on the page | light | 69.8 | 4.52:1 | Pass |
| the card hairline on the page | dark | -28.7 | 4.19:1 | Below floor |
| the card hairline on a card | light | 72.8 | 4.72:1 | Pass |
| the card hairline on a card | dark | -27.9 | 3.80:1 | Below floor |
These are the measured token pairs this component draws colour from, not a measurement of the component itself.
Data attributes
| Attribute | Condition | Value |
|---|---|---|
| data-slot | On the root and every inner part | switch, switch-label, switch-description, switch-control, switch-thumb |
| data-checked / data-unchecked | On the control and the thumb, one or the other | Present with no value; reflects checked |
| data-disabled | On the control while disabled | Present 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.
Related
- 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.
IconButton
A button whose only visible content is an icon, so it always requires an accessible name you supply. Prefer a labelled button wherever there is room for a word.
Checkbox
A single labelled box a reader ticks, unticks or leaves in a mixed state, built on Base UI Checkbox and carrying neither colour axis.