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

  1. Keep a visible gap between the thumb and track edge; a flush thumb reads as a flat pill and fails low-vision contrast checks.
  2. Give the control a visible size identity too: some instruments still respect a larger cursor target by pointer than by trackpad.
  3. 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.