Web accessibility (a11y) ensures websites and apps work for everyone — including people with visual, motor, auditory, and cognitive disabilities. In 2026, WCAG 2.2 is the global standard, and accessibility lawsuits have made a11y a legal requirement for most businesses. This guide covers the essentials every developer must know.
📋 Table of Contents
Why Accessibility Matters
- 1 billion people worldwide have a disability
- Legal requirement in US (ADA), EU (EAA), and most countries
- SEO benefit — screen reader friendly code is also search engine friendly
- Better UX for everyone — captions help in loud environments, keyboard nav helps power users
WCAG 2.2 — The Four Principles (POUR)
- Perceivable — users can see/hear content (alt text, captions)
- Operable — users can navigate (keyboard, no seizure-inducing content)
- Understandable — content is clear (readable text, error messages)
- Robust — works with assistive technologies
Essential Techniques
1. Semantic HTML
<!-- BAD: div soup -->
<div class="header">
<div class="nav">
<div onclick="go('/')">Home</div>
</div>
</div>
<!-- GOOD: semantic HTML -->
<header>
<nav aria-label="Main navigation">
<a href="/">Home</a>
</nav>
</header>
<!-- Use semantic elements for meaning -->
<main> <!-- primary content -->
<article> <!-- self-contained content -->
<section> <!-- thematic grouping -->
<aside> <!-- tangentially related -->
<figure> <!-- image with caption -->
<figcaption> <!-- caption for figure -->
<button> <!-- interactive element (not div!) -->
2. Images and Alt Text
<!-- Informative image: describe what image conveys -->
<img src="chart.png" alt="Bar chart showing 40% increase in revenue Q1 2026" />
<!-- Decorative image: empty alt (screen readers skip) -->
<img src="divider.svg" alt="" role="presentation" />
<!-- Complex image: describe in text or use longdesc -->
<figure>
<img src="complex-graph.png" alt="Architecture diagram described below" />
<figcaption>
System architecture with three tiers: frontend (React),
API (FastAPI), and database (PostgreSQL).
</figcaption>
</figure>
<!-- Icon with text: hide icon from AT -->
<button>
<svg aria-hidden="true" focusable="false">...</svg>
Download PDF
</button>
<!-- Icon only: add visually hidden label -->
<button aria-label="Close dialog">
<svg aria-hidden="true">...</svg>
</button>
3. Keyboard Navigation
<!-- ALL interactive elements must be keyboard accessible -->
<!-- Use button for actions, a for navigation -->
<button onclick="openModal()">Open</button> <!-- enter/space activates -->
<a href="/about">About</a> <!-- enter activates -->
<!-- Custom interactive element: add tabindex + keyboard handler -->
<div
role="button"
tabindex="0"
onclick="handleClick()"
onkeydown="if(event.key==='Enter'||event.key===' ')handleClick()"
>
Custom Button
</div>
<!-- Skip link: helps keyboard users jump to main content -->
<a href="#main-content" class="skip-link">Skip to main content</a>
<header>...</header>
<main id="main-content">...</main>
<!-- Never remove focus outline without replacement -->
/* BAD */
:focus { outline: none; }
/* GOOD */
:focus-visible {
outline: 3px solid #0066CC;
outline-offset: 2px;
}
4. ARIA (Accessible Rich Internet Applications)
<!-- Use ARIA only when HTML semantics aren't enough -->
<!-- aria-label: provides accessible name -->
<button aria-label="Close dialog">✕</button>
<!-- aria-labelledby: references visible text -->
<h2 id="dialog-title">Confirm Delete</h2>
<div role="dialog" aria-labelledby="dialog-title">...</div>
<!-- aria-describedby: provides description -->
<input
id="email"
type="email"
aria-describedby="email-hint"
/>
<p id="email-hint">We'll never share your email.</p>
<!-- aria-live: announce dynamic content changes -->
<div aria-live="polite" aria-atomic="true">
<!-- Screen reader announces when content changes -->
Form submitted successfully!
</div>
<!-- aria-expanded: collapsible elements -->
<button aria-expanded="false" aria-controls="menu">Menu</button>
<ul id="menu" hidden>...</ul>
<!-- aria-invalid: form validation -->
<input aria-invalid="true" aria-describedby="email-error" />
<p id="email-error" role="alert">Invalid email format</p>
5. Color and Contrast
/* WCAG AA: 4.5:1 for normal text, 3:1 for large text */
/* WCAG AAA: 7:1 for normal text */
/* Check contrast: use WebAIM Contrast Checker */
/* Good contrast */
color: #1a1a1a; /* very dark gray on white: 18.1:1 */
background: #0066CC;
color: white; /* white on blue: 5.4:1 — AA pass */
/* Bad contrast (avoid) */
color: #aaa; /* light gray on white: 2.3:1 — FAIL */
/* Never rely on color alone to convey information */
/* Bad: red text = error, green = success */
/* Good: add icon + text label + color */
.error::before { content: "⚠ Error: "; }
/* Focus indicators must meet contrast requirements too */
:focus-visible {
outline: 3px solid #0066CC; /* sufficient contrast */
}
Testing Accessibility
# Automated testing (catches ~30-40% of issues)
npm install --save-dev axe-core @axe-core/playwright
# Run axe in Playwright
import { checkA11y } from 'axe-playwright';
await checkA11y(page, null, { runOnly: ['wcag2a', 'wcag2aa'] });
# Browser extensions
# - axe DevTools (Chrome/Firefox) — most comprehensive
# - Lighthouse (Chrome DevTools) → Accessibility score
# - WAVE (WebAIM) — visual overlay
# Manual testing checklist:
# ☐ Navigate entire page with Tab key only
# ☐ Test with screen reader (NVDA/JAWS Windows, VoiceOver Mac/iOS)
# ☐ Zoom to 200% — does nothing overlap?
# ☐ Turn off CSS — is content readable in order?
# ☐ Check color contrast with DevTools
React Accessibility
// React-specific a11y patterns
// 1. Form labeling
function EmailInput() {
return (
<div>
<label htmlFor="email">Email address</label>
<input id="email" type="email" autoComplete="email" />
</div>
);
}
// 2. Focus management in modals
function Dialog({ isOpen, onClose }: Props) {
const dialogRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (isOpen) dialogRef.current?.focus(); // trap focus
}, [isOpen]);
return isOpen ? (
<div
role="dialog"
aria-modal="true"
aria-labelledby="dialog-title"
tabIndex={-1}
ref={dialogRef}
>
<h2 id="dialog-title">Confirm Action</h2>
<button onClick={onClose}>Cancel</button>
</div>
) : null;
}
// 3. Live region for dynamic content
function StatusMessage({ message }: { message: string }) {
return (
<div aria-live="polite" aria-atomic="true">
{message}
</div>
);
}
Web accessibility in 2026 is both a legal requirement and a business advantage. Start with semantic HTML (80% of a11y comes from correct HTML), ensure keyboard navigability, add alt text to all images, and maintain color contrast. Use axe DevTools for automated checks and test with VoiceOver/NVDA for real-world validation.
📚 You might also like
🔗 Share this article




✍️ Leave a Comment