Toggle Switch
An accessible toggle switch component built with HTML, CSS, and minimal JavaScript.
Basic Toggle Switches
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
, andaria-describedby
attributes ensure screen readers can understand the component's state and purpose. -
Dual state management: Uses both native
checked
attribute andaria-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:
- Keyboard interaction: CSS cannot detect keyboard events like Enter or Space. Only JavaScript can listen for these events and trigger the toggle.
-
ARIA state updates: CSS cannot dynamically
update ARIA attributes like
aria-checked
when the switch state changes. -
State synchronization: JavaScript ensures both
the native
checked
attribute andaria-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 andaria-checked
on the label withrole="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 therole="switch"
on the label - Both states are kept in sync through JavaScript
-
The native
-
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);
}
}
});
});
});