machine coding11
react ui components
controlled-componentscssaccessibility

Toggle switch

Build an accessible toggle switch from a checkbox.

easy1 min readsolution

What it tests

Whether you reach for a native input before building one out of divs. The whole component is a styled <input type="checkbox"> — which means focus, keyboard activation and screen-reader semantics come for free.

The approach

A fully controlled component: isOn in, onToggle out, no internal state. The real checkbox is visually hidden and a sibling span is painted as the track:

export function Switch({ isOn, onToggle, label }) {
  return (
    <label className="switch">
      <input type="checkbox" checked={isOn} onChange={onToggle} />
      <span className="slider" />
      <span className="text">{label}</span>
    </label>
  );
}

Wrapping everything in <label> makes the text part of the hit target without needing an explicit htmlFor.

Where it usually goes wrong

  • <div onClick>. Not focusable, not keyboard-operable, announces nothing.
  • display: none on the input. Removes it from the tab order too. Clip it with opacity: 0 or a sr-only pattern instead.
  • checked without onChange. React makes the input read-only and warns.

Full solution

3 files from src/folders/switch.

index.jsx

export { Switch } from "./switch";

switch.jsx

import "./switch.css";
 
export function Switch({ isOn, onToggle, label }) {
  return (
    <label className="switch">
      <input type="checkbox" checked={isOn} onChange={onToggle} />
      <span className="slider" />
      <span className="text">{label}</span>
    </label>
  );
}

switch.css

.switch {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  cursor: pointer;
}
 
.switch input {
  display: none;
}
 
.slider {
  width: 50px;
  height: 26px;
  background: #ccc;
  border-radius: 999px;
  position: relative;
  transition: 0.2s;
}
 
.slider::before {
  content: "";
  position: absolute;
  height: 20px;
  width: 20px;
  top: 3px;
  left: 3px;
  background: white;
  border-radius: 50%;
  transition: 0.2s;
}
 
.switch:has(input:checked) .slider {
  background: black;
}
 
.switch:has(input:checked) .slider::before {
  transform: translateX(24px);
}
 
.text {
  font-size: 14px;
}
share this post