skip to content

Accessible Toggle Buttons

Toggle buttons are a pleasant interface for toggling a value between two states, and offer the same semantics and keyboard navigation as native checkbox elements.

Example

Checked Toggle Buttons
Unchecked Toggle Buttons
Disabled Toggle Buttons

Markup (HTML)

<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>

Styles (CSS)

.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;
    }
  }
}