The Complete Guide to Building Accessible Web Apps in 2026: ARIA, Focus Management, and Screen Readers
Web accessibility became a legal requirement in more jurisdictions by 2025-2026, and the business case is stronger than ever — 15-20% of the population has some form of disability. Building accessible apps isn't just ethical, it's now expected by employers and users alike.
Here's the practical guide.
Semantic HTML First
<!-- Bad -->
<div onclick="toggle()">
<div class="btn">Click me</div>
</div>
<!-- Good -->
<button type="button" onclick="toggle()">Click me</button>
<!-- Native elements have built-in keyboard support and ARIA for free -->
<nav> → role="navigation"
<main> → role="main"
<aside> → role="complementary"
Focus Management
/* Visible focus indicator (never remove, only style) */
:focus-visible {
outline: 2px solid #4f46e5;
outline-offset: 2px;
}
/* Skip link */
.skip-link {
position: absolute;
top: -40px;
left: 0;
padding: 8px 16px;
background: #000;
color: #fff;
z-index: 100;
}
.skip-link:focus {
top: 0;
}
<a href="#main-content" class="skip-link">Skip to main content</a>
<main id="main-content" tabindex="-1">
<!-- Page content -->
</main>
ARIA Attributes
<!-- Live region for dynamic updates -->
<div aria-live="polite" aria-atomic="true">
<!-- Screen reader announces changes here -->
<p>5 items in cart</p>
</div>
<!-- Expanded/collapsed -->
<button
aria-expanded="false"
aria-controls="menu-dropdown"
onclick="toggleMenu()"
>
Menu
</button>
<div id="menu-dropdown" hidden>
<!-- Menu items -->
</div>
<!-- Progress bar -->
<div role="progressbar"
aria-valuenow="65"
aria-valuemin="0"
aria-valuemax="100"
aria-label="Upload progress">
65%
</div>
Keyboard Navigation
// Dropdown keyboard navigation
const menuItem = menu.querySelectorAll("li");
menu.addEventListener("keydown", (e) => {
const currentIndex = Array.from(menuItem).indexOf(document.activeElement);
switch (e.key) {
case "ArrowDown":
e.preventDefault();
const next = menuItem[currentIndex + 1] || menuItem[0];
next.focus();
break;
case "ArrowUp":
e.preventDefault();
const prev = menuItem[currentIndex - 1] || menuItem[menuItem.length - 1];
prev.focus();
break;
case "Escape":
closeMenu();
trigger.focus();
break;
}
});
Color Contrast
/* Pass WCAG AA (4.5:1 for text, 3:1 for large text) */
.text {
color: #333; /* on white background */
/* Contrast ratio: 12.6:1 ✅ */
}
/* Don't rely on color alone */
.status {
color: #d00; /* Also include icon or text */
background: #fee;
}
/* Better: use both color AND icon */
<span class="status error">
✗ Error occurred
</span>
Testing Tools
# axe-core (automated testing)
# Chrome: axe DevTools extension
# CLI: npx @axe-core/cli https://example.com
# Lighthouse accessibility audit
# In Chrome DevTools → Lighthouse → "Navigation" → check "Accessibility"
# Screen reader testing
# macOS: VoiceOver (Cmd + F5)
# Windows: NVDA
# Linux: Orca
This article contains affiliate links. If you sign up through the links above, I may earn a commission at no additional cost to you.
Ready to Build Your Online Business?
Get started with Systeme.io for free — All-in-one platform for building your online business with AI tools.
Top comments (0)