Toggle Switch

An accessible toggle switch component built with HTML, CSS, and minimal JavaScript.

Basic Toggle Switches

Basic toggle switches
Off position toggle switch example
On position toggle switch example

Keyboard users: Use Tab to navigate between toggle switches and Space to toggle them on and off.

How It Works

This toggle switch component combines HTML, CSS, and minimal JavaScript to create an accessible switch control. The implementation uses a hidden checkbox input with a styled label to create the visual appearance of a switch.

The component uses both native HTML attributes and ARIA attributes to ensure maximum compatibility across different screen readers and assistive technologies.

Key Accessibility Features

  • Keyboard navigation: All toggle switches are focusable with tabindex="0". Users can press Space or Enter to toggle the switch.
  • ARIA attributes: Proper role="switch", aria-checked, and aria-describedby attributes ensure screen readers can understand the component's state and purpose.
  • Dual state management: Uses both native checked attribute and aria-checked for maximum compatibility.
  • Screen reader support: The component structure is semantically correct for optimal screen reader announcements.

Why JavaScript is Necessary

While we've minimized JavaScript usage, there are key limitations with a CSS-only approach:

  1. Keyboard interaction: CSS cannot detect keyboard events like Enter or Space. Only JavaScript can listen for these events and trigger the toggle.
  2. ARIA state updates: CSS cannot dynamically update ARIA attributes like aria-checked when the switch state changes.
  3. State synchronization: JavaScript ensures both the native checked attribute and aria-checked stay in sync.

Our JavaScript is minimal and focuses solely on:

  • Handling keyboard events (Enter/Space) to toggle switches
  • Updating the aria-checked attribute to match the switch's state
  • Keeping the native checked attribute and ARIA states synchronized

Without this JavaScript, the toggle switch would still work for mouse users but would fail accessibility requirements for keyboard-only users.

Accessibility Features

This toggle switch component includes several accessibility features:

  • Semantic HTML: Using native checkbox inputs for proper form submission and keyboard accessibility
  • Proper Labels: Each toggle is associated with a label for screen reader support
  • Screen Reader Text: Hidden text provides context for screen reader users
  • Keyboard Accessibility: Toggles can be activated with the Space key when focused
  • Focus Indicators: Clear visual focus indicators for keyboard navigation
  • Sufficient Contrast: Colors meet WCAG contrast requirements
  • Touch-friendly: Adequately sized touch targets for mobile users

ARIA Implementation

The toggle switch component implements several ARIA attributes to ensure proper accessibility:

  • role="switch": Applied to the label element to indicate that it functions as a switch control, similar to a light switch. This helps screen readers understand the component's purpose and behavior.
  • Dual State Management: We use both the native checked attribute on the input and aria-checked on the label with role="switch". This ensures maximum compatibility across different screen readers and assistive technologies:
    • The native checked attribute works with the checkbox input
    • The aria-checked attribute works with the role="switch" on the label
    • Both states are kept in sync through JavaScript
  • aria-describedby: Used to associate descriptive text with each toggle switch, providing additional context for screen reader users about the purpose of each switch.
  • tabindex="0": Added to the label to ensure it's focusable and can be activated using keyboard navigation.
  • role="group": Applied to the container of multiple toggle switches to indicate that they form a logical group.
  • aria-labelledby: Used on the group container to associate it with a heading that describes the group of toggle switches.

Implementation Code

HTML Structure

<!-- Basic Toggle Switch -->
<div class="toggle-switch">
  <input type="checkbox" id="switch1" class="toggle-input" aria-describedby="switch1-desc" />
  <label for="switch1" class="toggle-label" role="switch" aria-checked="false" tabindex="0">
    <span class="toggle-button"></span>
    <span class="sr-only">Toggle switch</span>
  </label>
  <span id="switch1-desc" class="sr-only">Toggle switch description</span>
</div>

CSS Implementation

/* Toggle Switch Container */
.toggle-switch {
  position: relative;
  display: inline-block;
}

/* Hide the default checkbox */
.toggle-input {
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
}

/* The switch track (background) */
.toggle-label {
  display: block;
  width: 3.5rem;
  height: 1.75rem;
  border-radius: 1.75rem;
  background-color: #ccc;
  cursor: pointer;
  transition: background-color 0.3s;
  position: relative;
}

/* The switch button (the circle) */
.toggle-button {
  position: absolute;
  top: 0.25rem;
  left: 0.25rem;
  width: 1.25rem;
  height: 1.25rem;
  border-radius: 50%;
  background-color: white;
  transition: transform 0.3s;
}

/* When checked - change track color */
.toggle-input:checked + .toggle-label,
.toggle-label[aria-checked="true"] {
  background-color: var(--primary-color, #0077cc);
}

/* When checked - move the button */
.toggle-input:checked + .toggle-label .toggle-button,
.toggle-label[aria-checked="true"] .toggle-button {
  transform: translateX(1.75rem);
}

/* Focus styles for accessibility */
.toggle-input:focus-visible + .toggle-label,
.toggle-label:focus-visible {
  outline: 2px solid var(--primary-color, #0077cc);
  outline-offset: 2px;
}

JavaScript Implementation

document.addEventListener("DOMContentLoaded", function () {
  const toggleLabels = document.querySelectorAll('label[role="switch"]');
  
  toggleLabels.forEach((label) => {
    // Handle click events
    label.addEventListener("click", function (e) {
      e.preventDefault();
      const input = document.getElementById(this.getAttribute("for"));
      if (input) {
        input.checked = !input.checked;
        // Update aria-checked to match the checked state
        this.setAttribute("aria-checked", input.checked);
      }
    });
    
    // Handle keyboard events (Space and Enter)
    label.addEventListener("keydown", function (e) {
      if (e.key === " " || e.key === "Enter") {
        e.preventDefault();
        const input = document.getElementById(this.getAttribute("for"));
        if (input) {
          input.checked = !input.checked;
          // Update aria-checked to match the checked state
          this.setAttribute("aria-checked", input.checked);
        }
      }
    });
  });
});

Additional Resources