Switch
A native switch built with a real <input type="checkbox">. The label toggles the control, while CSS state hooks style the track and thumb from the checkbox state.
0 KB JavaScript
~30 CSS lines
checkbox native state
keyboard native access
Preview
The code
The markup
<label class="switch">
<input class="switch-input" type="checkbox" checked>
<span class="switch-track" aria-hidden="true"></span>
<span class="switch-label">Switch title</span>
</label>CSS mechanisms
/* ----------------------------------------------------------------------------
* Switch Layout
* ---------------------------------------------------------------------------- */
/**
* Switch row: label + hidden checkbox + visual track.
*/
.switch {
--switch-thumb-translate: 20px;
align-items: center;
cursor: pointer;
display: inline-flex;
/* add your styles here */
}
/**
* Visually hide the actual checkbox while keeping it accessible
* (still reachable by keyboard and screen readers via the label).
*/
.switch-input {
block-size: 1px;
inline-size: 1px;
opacity: 0;
pointer-events: none;
position: absolute;
}
/* ----------------------------------------------------------------------------
* Switch Track & Thumb
* ---------------------------------------------------------------------------- */
/**
* Track.
*/
.switch-track {
position: relative;
/* add your styles here */
}
/**
* Thumb.
*/
.switch-track::after {
content: '';
position: absolute;
transition: transform 240ms cubic-bezier(0.22, 1, 0.36, 1);
/* add your styles here */
}
/* ----------------------------------------------------------------------------
* Switch State hooks
* ---------------------------------------------------------------------------- */
/**
* Hover.
*/
.switch:hover {
/* add your styles here */
}
/**
* Focus.
*/
.switch-input:focus-visible + .switch-track {
/* add your focus styles here */
}
/**
* Active (checked) state. `:has()` lets the wrapper
* react to the inner input's `:checked` state.
*/
.switch:has(.switch-input:checked) {
/* add your styles here */
}
/**
* Checked-state track.
*/
.switch:has(.switch-input:checked) .switch-track {
/* add your checked track styles here */
}
/**
* Checked-state thumb position.
*/
.switch:has(.switch-input:checked) .switch-track::after {
transform: translateX(var(--switch-thumb-translate));
}Benefits
-
01.
Native control
The real
<input>keeps browser-managed checked state, form behavior, keyboard access, and change events. -
02.
Label activation
Wrapping the input in a
<label>makes the full switch row clickable without extra JavaScript. -
03.
CSS-only state
The visual track and thumb react to
:checked,:focus-visible, and:has()without runtime state. -
04.
Style agnostic
The snippet only defines the mechanics. Size, colors, spacing, focus ring, and motion remain fully themeable.
Current limitations
-
01.
Switch semantics
A checkbox behaves correctly, but may still be announced as a checkbox. Use dedicated switch semantics only when the project needs that exact assistive technology output.
-
02.
Parent styling support
Styling the parent depends on
:has(). For older browsers, basic track and thumb states can still use:checked + .switch-track. -
03.
Hidden input care
The checkbox must stay focusable and associated with its label. Avoid
display: noneor removing visible focus styles.