Switch
A setting you flip on or off, effective immediately.
import { Switch } from "@dede-style/ui/switch", then <Switch checked={on} onChange={setOn} label="Show guides" />. It is controlled — the value lives with whoever owns the setting, and this renders it.
A switch takes effect the moment you move it. That is what separates it from a checkbox, which collects your answer and waits for a submit. If the setting needs an Apply button, it is not a switch.
It stands in the same row as a Checkbox or a Radio: the words are part of it, the whole row is the target, and the switch centres on the label’s first line.
It is a checkbox underneath
The obvious build is <button role="switch">, and it is the one that cannot be corrected later. A button can never take part in a form — no name, no value, nothing to submit — so the day a form needs one, swapping the element is a breaking change rather than a refactor.
Starting from the checkbox costs nothing and arrives with three things already done: Space toggles it, disabled means disabled, and :checked exists — which is what lets the thumb’s position be a CSS rule rather than React state driving a visual. The whole control is the input and one pseudo-element: the input is the track, because it has to exist anyway, and the thumb is decoration, which is what a pseudo-element is for.
role="switch" sits on top of it, and it is the one thing that would be cheap to remove. A checkbox says included; a switch says on. That distinction only exists for someone listening rather than looking, and if it ever reads badly, deleting one attribute leaves a perfectly good checkbox behind.
States
Four to read, because both settings have to survive being dimmed.
- Off
- On
- Off, disabled
- On, disabled
The track carries the state. Off, it is set into the page with a visible edge; on, it is solid ink. The thumb changes with it, and that is not decoration. A single thumb colour seems natural — a physical object does not change colour when it moves — but it fails in both modes at once. A pale thumb on a pale off-track measures barely above 1:1, and on the dark mode there is almost no room below a near-black page for a shadow to rescue it.
Disabled is one opacity on the whole control, so the track, the edge and the thumb dim together and keep their relationship to each other. A set of disabled colours would dim them one at a time and let the ratios drift apart.
Hovering anywhere on the row washes the track — the same film a button takes, laid over whichever fill the state has, so one rule serves on and off alike.
Accessibility
The label is the name; the description is a description.
label is required. It is the visible words and the accessible name in one string, so the two cannot drift; the description is announced after it, never folded in. With hideLabel the words leave the screen and stay in the name — for a switch whose name is already on screen, like the ones in a props table.
Name the setting, not the state. “Show guides”, never “Guides on” — the switch already reports which way it is set, and a name that repeats it is a status readout wearing a control’s clothes. It is also the thing that goes wrong when the name is written to match the current value: it then describes the opposite of what pressing it will do.
The switch follows its words — controlPosition defaults to end, the settings convention, where a checkbox defaults to start. The row hugs its content, so empty space beside a short label is not a click target; fullWidth spreads it across its container, which is the list above — names on one edge, switches on the other.
Sizes
Checkbox’s three heights; the width follows.
16, 20 and 24 tall — size 4, 5 and 6, the same steps as a checkbox. The width keeps a 5:3 proportion rounded to the 4px grid, so 28, 32 and 40, and the thumb leaves 4px of track all round. 6 is the default and the one for touch: 4 and 5 sit under the 24px target, so they are for a dense, pointer-first surface — the guides switch in a layout page’s toolbar is a 5.