Toggle switches fall to JavaScript libraries because a rounded thumb sliding on a track looks scripted. It is one checkbox wearing CSS: appearance none clears the native checkbox art, and the pseudo-elements draw track and thumb.
input[type="checkbox"].switch {
appearance: none;
inline-size: 3rem;
block-size: 1.7rem;
border-radius: 9999px;
background: oklch(0.85 0 0);
position: relative;
transition: background 0.2s;
}
input[type="checkbox"].switch::before {
content: "";
position: absolute;
inset-inline-start: 0.2rem;
block-size: 1.3rem;
inline-size: 1.3rem;
border-radius: inherit;
background: white;
transition: translate 0.2s;
}
input[type="checkbox"].switch:checked {
background: var(--brand);
}
input[type="checkbox"].switch:checked::before { translate: 1.3rem 0; }What you keep for free
- Form semantics: it is still an input element with checked state, labels, radios-in-groups, everything forms know how to do.
- Keyboard behavior: Space toggles; Tab reaches; the focus-visible ring wraps the whole control because it is the input.
- Reduced-motion handling: both transition lines sit inside the shared gate when the stylesheet routes animation.
- Disabled and dark-mode support through the same custom property palette the rest of the site already drinks from.
The details that separate fine from excellent
- Keep a visible gap between the thumb and track edge; a flush thumb reads as a flat pill and fails low-vision contrast checks.
- Give the control a visible size identity too: some instruments still respect a larger cursor target by pointer than by trackpad.
- Test unchecked, checked, disabled and focus-visible, then flip the OS-level dark mode and re-check.
A switch that never ships a byte of JS is one of those gacor platform wins: the HTML holds the meaning, the CSS draws the fashion.
