react ui components
controlled-componentscssaccessibility
Toggle switch
Build an accessible toggle switch from a checkbox.
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: noneon the input. Removes it from the tab order too. Clip it withopacity: 0or asr-onlypattern instead.checkedwithoutonChange. 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