Why Build a CSS Toggle Switch?
Toggle switches are a staple of modern web interfaces, but native HTML doesn't provide one out of the box. The good news: with a checkbox input, a label, and some CSS, you can build a polished, functional toggle switch without writing a single line of JavaScript. This tutorial walks through the complete process.
The HTML Structure
The trick is to use a hidden checkbox as the state holder, paired with a <label> that acts as the visual toggle. Clicking the label toggles the checkbox, which we then style with CSS.
<label class="toggle-switch">
<input type="checkbox" role="switch" aria-checked="false">
<span class="slider"></span>
<span class="label-text">Enable Dark Mode</span>
</label>
Note the role="switch" and aria-checked attributes — these are important for accessibility (more on this below).
The Core CSS
Here's the foundational CSS to style the toggle:
.toggle-switch {
display: inline-flex;
align-items: center;
gap: 12px;
cursor: pointer;
}
.toggle-switch input {
display: none; /* Hide the real checkbox */
}
.slider {
position: relative;
width: 52px;
height: 28px;
background-color: #ccc;
border-radius: 14px;
transition: background-color 0.25s ease;
}
.slider::before {
content: '';
position: absolute;
width: 22px;
height: 22px;
border-radius: 50%;
background: white;
top: 3px;
left: 3px;
transition: transform 0.25s ease;
box-shadow: 0 1px 4px rgba(0,0,0,0.3);
}
input:checked + .slider {
background-color: #2D6BE4;
}
input:checked + .slider::before {
transform: translateX(24px);
}
How It Works
- The real
<input type="checkbox">is hidden withdisplay: none. - The
.sliderspan creates the visual track (the pill shape). - The
.slider::beforepseudo-element creates the circular thumb. - When the checkbox is
:checked, the+adjacent sibling selector targets the slider and changes its background color and moves the thumb viatranslateX. - CSS transitions animate the color change and movement smoothly.
Adding a Focus Style for Keyboard Users
Because the real input is hidden, you need to restore visible focus styling for keyboard navigation:
input:focus-visible + .slider {
outline: 3px solid #2D6BE4;
outline-offset: 2px;
}
This ensures keyboard users see a clear focus ring when tabbing to the toggle.
Adding a Disabled State
input:disabled + .slider {
opacity: 0.5;
cursor: not-allowed;
}
.toggle-switch:has(input:disabled) {
cursor: not-allowed;
}
Customization Tips
- Size: Scale the
width,height, and thumb size proportionally. A good thumb-to-track ratio is roughly 80% of the track height. - Colors: Change the
:checkedbackground to match your brand's primary color. - Icons: Place Unicode characters (✓ / ✕) inside the
.slider::beforepseudo-element usingcontentandfont-sizefor added visual context. - Animation speed: Adjust the
transitionduration — 200–300ms feels natural for most interfaces.
Browser Compatibility
This approach works in all modern browsers. The :has() selector used for the disabled parent requires Chrome 105+, Firefox 121+, or Safari 15.4+. For older browser support, apply cursor: not-allowed directly on the label element using a class instead.
Wrapping Up
A pure CSS toggle switch is lightweight, easy to maintain, and avoids JavaScript dependency for simple on/off states. Pair it with proper ARIA attributes and you have a component that's both visually polished and inclusive.