Web Accessibility (a11y): WCAG 2.2, Keyboard Navigation & ARIA Patterns
The comprehensive engineering runbook for digital accessibility: Comply with WCAG 2.2 AA standards, master accessible keyboard focus management, trap focus inside modal dialogs, and apply ARIA patterns responsibly.
1. The First Rule of ARIA: Semantic HTML First
<!-- ANTI-PATTERN: Accessible nightmare requiring 20 lines of custom JS -->
<div class="custom-btn" onclick="submitForm()">Submit</div>
<!-- Missing: tabIndex, Enter key handler, Space key handler, role="button", disabled state! -->
<!-- PRODUCTION PATTERN: 100% accessible natively across all OS and devices -->
<button type="button" class="btn-primary" onclick="submitForm()">
Submit
</button>A native <button> handles keyboard activation (Enter and Space), focus outline, and disabled states out of the box.
| Component | Native Semantic HTML | Broken ARIA Anti-Pattern | Copy |
|---|---|---|---|
| Action trigger | <button type="button"> | <div role="button" tabindex="0"> | |
| Page Navigation | <a href="/url"> | <span onclick="navigate()"> (Unreachable via Tab) | |
| Checkbox | <input type="checkbox"> | <div class="checkbox" aria-checked="true"> | |
| Expandable Drawer | <details><summary>Title</summary>...</details> | Custom JS accordion with fragile ARIA states |
2. Focus Management & The :focus-visible Standard
/* NEVER DO THIS:
* :focus { outline: none; } -> Violates WCAG 2.4.7 Focus Visible!
*/
/* PRODUCTION PATTERN: Crisp keyboard-only focus ring with 3:1 contrast */
:focus-visible {
outline: 2px solid #2563eb; /* High-contrast brand blue */
outline-offset: 2px; /* Separates outline from element border */
}
/* For dark backgrounds */
:global(.dark) :focus-visible {
outline-color: #60a5fa;
}
/* Skip to Content Link (Essential for keyboard power users) */
.skip-to-content {
position: absolute;
left: -9999px;
top: 1rem;
z-index: 9999;
padding: 0.75rem 1.5rem;
background: #043064;
color: #ffffff;
border-radius: 0.5rem;
}
.skip-to-content:focus {
left: 1rem; /* Springs into view on first Tab press! */
}A skip-to-content link allows keyboard users to bypass repetitive top navigation bars and jump straight to main content.
3. Building Accessible Modals & Focus Trapping
function openAccessibleModal(modalElement, triggerButton) {
const focusable = modalElement.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstEl = focusable[0];
const lastEl = focusable[focusable.length - 1];
modalElement.setAttribute("aria-modal", "true");
modalElement.setAttribute("role", "dialog");
firstEl.focus();
function handleKeyDown(e) {
if (e.key === "Escape") {
closeModal();
return;
}
if (e.key === "Tab") {
if (e.shiftKey && document.activeElement === firstEl) {
e.preventDefault();
lastEl.focus(); // Wrap backwards to last element
} else if (!e.shiftKey && document.activeElement === lastEl) {
e.preventDefault();
firstEl.focus(); // Wrap forward to first element
}
}
}
modalElement.addEventListener("keydown", handleKeyDown);
function closeModal() {
modalElement.removeEventListener("keydown", handleKeyDown);
modalElement.classList.add("hidden");
triggerButton.focus(); // CRITICAL: Restore focus!
}
}Always save and restore the triggerButton focus upon closing overlays to maintain user location.
4. WCAG 2.2 Color Contrast & Motion Preferences
/* Respect users with vestibular disorders by disabling dizzying transitions */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}prefers-reduced-motion is an essential accessibility media query honored by all modern operating systems.
5. Dynamic Updates with aria-live Regions
<!-- 1. Politeness Level: "polite" waits until user stops speaking -->
<div role="status" aria-live="polite" class="sr-only" id="toastAnnouncement">
<!-- Insert text via JS: "Copied to clipboard!" -->
</div>
<!-- 2. Politeness Level: "assertive" interrupts immediately for critical errors -->
<div role="alert" aria-live="assertive" class="error-banner">
Network connection lost. Please verify your internet connection.
</div>
<!-- Screen reader only utility class: visually hidden but accessible -->
<style>
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
</style>Use aria-live="polite" for copy confirmations and calculation results; reserve role="alert" for fatal failures.
Related Free Online Tools
Put these techniques into practice immediately with our privacy-first browser utilities:
Inspect JavaScript KeyboardEvent parameters, event.key, event.code, and key modifier combinations in real-time.
Detect and sanitize hidden zero-width spaces and Unicode artifacts that distort screen readers.
Analyze text readability, reading time metrics, and semantic keyword clarity for assistive users.