Accessible Toggle Buttons


Designing toggle buttons that are not only beautiful but also accessible is crucial for modern web interfaces. Below is a minimal example that strikes a balance between aesthetics and inclusivity.

Why Accessibility Matters

Toggle switches often serve critical functions (think dark mode, email preferences, or privacy settings). Without proper accessibility, keyboard users, screen readers, and assistive technologies might struggle to interact with them.

This demo supports:

  • Keyboard navigation (using Tab and Space)
  • Screen reader cues (via ARIA roles and states)
  • Visual clarity for different states (checked, unchecked, and disabled)

Example

Checked Toggle Buttons
Unchecked Toggle Buttons
Disabled Toggle Buttons

Markup (HTML)

Each toggle is wrapped in a .toggle container with an input[type=checkbox] and a

<div class="toggle">
  <input type="checkbox" id="checked" class="toggle-input" checked />
  <label for="checked" role="checkbox" aria-checked="true" tabindex="0" aria-labelledby="checked" class="toggle-label"></label>
  <span class="toggle-text">Checked Toggle Buttons</span>
</div>
<div class="toggle">
  <input type="checkbox" id="unchecked" class="toggle-input" />
  <label for="unchecked" role="checkbox" aria-checked="false" tabindex="0" aria-labelledby="unchecked" class="toggle-label"></label>
  <span class="toggle-text">Unchecked Toggle Buttons</span>
</div>
<div class="toggle">
  <input type="checkbox" id="unchecked_disabled" class="toggle-input" disabled />
  <label for="unchecked_disabled" role="checkbox" aria-checked="false" tabindex="0" aria-labelledby="unchecked_disabled" class="toggle-label"></label>
  <span class="toggle-text">Disabled Toggle Buttons</span>
</div>

Key Accessibility Features

  • role=“checkbox” helps screen readers interpret the toggle state correctly.
  • aria-checked dynamically reflects the toggle’s current state.
  • tabindex=“0” ensures the toggle can be focused via keyboard.
  • Label and input are linked via for/id and aria-labelledby.

Styles (CSS)

The toggle UI is entirely custom, with smooth transitions and a neutral color palette.

.toggle {
  --color-1: #eceeef;
  --color-2: #777f86;
  --color-3: #313436;
  --color-4: #E2001A;
  --color-5: #fff6f6;

  margin-block: 20px;
  display: flex;
}
.toggle .toggle-label {
  position: relative;
  display: block;
  height: 20px;
  width: 40px;
  background: var(--color-1);
  border-radius: 100px;
  cursor: pointer;
  transition: all 300ms ease;
}
.toggle .toggle-label::after {
  position: absolute;
  left: 0px;
  top: 0px;
  display: block;
  width: 20px;
  height: 20px;
  border-radius: 100px;
  background: var(--color-2);
  box-shadow: 0px 3px 3px rgba(0, 0, 0, 0.05);
  content: "";
  transition: all 300ms ease;
}
.toggle .toggle-label:active:after {
  transform: scale(1.15, 0.85);
}
.toggle .toggle-text {
  margin-left: 10px;
  font-size: 1rem;
  line-height: 20px;
  color: var(--color-3);
}
.toggle:has(.toggle-input:disabled) {
  cursor: not-allowed !important;
}
.toggle .toggle-input {
  display: none;
}
.toggle .toggle-input:checked ~ label {
  background: var(--color-1);
}
.toggle .toggle-input:checked ~ label:after {
  left: 20px;
  background: var(--color-4);
}
.toggle .toggle-input:disabled ~ label {
  background: var(--color-1);
  pointer-events: none;
}
.toggle .toggle-input:disabled ~ label:after {
  background: var(--color-5);
  cursor: not-allowed;
}

Functionality (JS)

const toggles = document.querySelectorAll('.toggle');
let label, input;

toggles.forEach((toggle) => {
  toggle.addEventListener('keydown', handler, false);
  toggle.children[1].addEventListener('click', handler, false);
});

function handler(e) {
  label = e.srcElement;
  input = label.previousElementSibling;
  
  if(e.type == 'keydown' && e.keyCode == 32 && input.disabled == false || e.type == 'click' && input.disabled == false) {
    e.preventDefault();
    if (input.checked == true) {
      label.setAttribute('aria-checked', 'false');
      label.setAttribute('aria-labelledby', 'unchecked');
      input.checked = false;
    } else {
      label.setAttribute('aria-checked', 'true');
      label.setAttribute('aria-labelledby', 'checked');
      input.checked = true;
    }
  }
}

This approach avoids overengineering — no libraries, just semantic HTML, thoughtful CSS, and a bit of JavaScript to handle interactions cleanly.

It’s small enough to drop into any project and scales well. You can easily extend this to support themes, animations, or form integrations.

Let me know if you want to package this into a live CodePen or component library format.