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

  1. The real <input type="checkbox"> is hidden with display: none.
  2. The .slider span creates the visual track (the pill shape).
  3. The .slider::before pseudo-element creates the circular thumb.
  4. When the checkbox is :checked, the + adjacent sibling selector targets the slider and changes its background color and moves the thumb via translateX.
  5. 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 :checked background to match your brand's primary color.
  • Icons: Place Unicode characters (✓ / ✕) inside the .slider::before pseudo-element using content and font-size for added visual context.
  • Animation speed: Adjust the transition duration — 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.