Web Accessibility
Web accessibility ensures that people with disabilities can perceive, understand, navigate, and interact with websites. Beyond being the right thing to do, accessibility is a legal requirement under laws like the ADA, Section 508, and the European Accessibility Act. The technical foundation is WCAG -- the Web Content Accessibility Guidelines.
Hub: Graphics & Web Design Related: Responsive Design | Web Performance
WCAG 2.1 AA Requirements
WCAG 2.1 Level AA is the most commonly targeted conformance level. It is organized around four principles (POUR):
- Perceivable -- Content must be presentable in ways users can perceive (text alternatives, captions, adaptable layout, distinguishable color/contrast).
- Operable -- UI must be operable via keyboard, with enough time, no seizure triggers, and navigable structure.
- Understandable -- Text must be readable, UI must be predictable, and input assistance must be provided.
- Robust -- Content must be compatible with current and future assistive technologies.
Key numeric targets: - Text contrast ratio: 4.5:1 for normal text, 3:1 for large text (18pt or 14pt bold). - Non-text contrast: 3:1 for UI components and graphical objects. - Target size: at least 24x24 CSS pixels (Level AA in WCAG 2.2).
Semantic HTML
Semantic HTML is the single most impactful accessibility technique. Assistive technologies rely on the document's semantic structure to build a navigable outline.
<header role="banner">
<nav aria-label="Primary">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
</header>
<main>
<article>
<h1>Page Title</h1>
<p>Introductory paragraph.</p>
<section aria-labelledby="features-heading">
<h2 id="features-heading">Features</h2>
<ul>
<li>Feature one description</li>
<li>Feature two description</li>
</ul>
</section>
</article>
<aside aria-label="Related links">
<h2>Related</h2>
<ul>
<li><a href="/docs">Documentation</a></li>
</ul>
</aside>
</main>
<footer role="contentinfo">
<p>© 2026 Example Corp</p>
</footer>
Use native elements whenever possible. A <button> is always preferable to a <div onclick> because it comes with focus management, keyboard activation, and an implicit ARIA role for free.
ARIA Roles, Properties, and States
ARIA (Accessible Rich Internet Applications) fills gaps where native HTML semantics fall short. The first rule of ARIA: do not use ARIA if a native HTML element will do the job.
<!-- Disclosure widget -->
<button aria-expanded="false" aria-controls="panel-1"
onclick="togglePanel(this)">
Show details
</button>
<div id="panel-1" role="region" aria-labelledby="panel-1-label" hidden>
<h3 id="panel-1-label">Details</h3>
<p>Additional content revealed on demand.</p>
</div>
<script>
function togglePanel(btn) {
const panel = document.getElementById(btn.getAttribute('aria-controls'));
const expanded = btn.getAttribute('aria-expanded') === 'true';
btn.setAttribute('aria-expanded', String(!expanded));
panel.hidden = expanded;
}
</script>
Common ARIA patterns:
<!-- Navigation landmark -->
<nav role="navigation" aria-label="Breadcrumb">
<ol>
<li><a href="/">Home</a></li>
<li><a href="/products">Products</a></li>
<li aria-current="page">Widget Pro</li>
</ol>
</nav>
<!-- Live region for dynamic updates -->
<div role="status" aria-live="polite" aria-atomic="true">
3 items added to cart
</div>
<!-- Tab interface -->
<div role="tablist" aria-label="Settings">
<button role="tab" aria-selected="true"
aria-controls="tab-general" id="tab-btn-general">General</button>
<button role="tab" aria-selected="false"
aria-controls="tab-advanced" id="tab-btn-advanced">Advanced</button>
</div>
<div role="tabpanel" id="tab-general"
aria-labelledby="tab-btn-general">
<p>General settings content.</p>
</div>
<div role="tabpanel" id="tab-advanced"
aria-labelledby="tab-btn-advanced" hidden>
<p>Advanced settings content.</p>
</div>
Keyboard Navigation
Every interactive element must be reachable and operable with a keyboard alone.
/* Visible focus indicators -- never remove outline without replacing it */
:focus-visible {
outline: 3px solid var(--color-primary);
outline-offset: 2px;
}
/* Remove the default outline only when replacing it */
button:focus:not(:focus-visible) {
outline: none;
}
Focus Management
When content changes dynamically (modals, SPAs, inline editing), you must manage focus programmatically:
// Trap focus inside a modal
function trapFocus(modal) {
const focusable = modal.querySelectorAll(
'a[href], button:not([disabled]), input, textarea, select, [tabindex]:not([tabindex="-1"])'
);
const first = focusable[0];
const last = focusable[focusable.length - 1];
modal.addEventListener('keydown', (e) => {
if (e.key !== 'Tab') return;
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
});
first.focus();
}
tabindex
tabindex="0"-- adds the element to the natural tab order.tabindex="-1"-- removes from tab order but allows programmatic focus (el.focus()).- Positive values -- avoid these; they override the natural order and create confusion.
Skip Links
Skip links let keyboard users bypass repetitive navigation blocks:
<body>
<a href="#main" class="skip-link">Skip to main content</a>
<header><!-- navigation --></header>
<main id="main" tabindex="-1">
<!-- page content -->
</main>
</body>
.skip-link {
position: absolute;
top: -100%;
left: 1rem;
padding: 0.75rem 1.5rem;
background: var(--color-primary);
color: #fff;
z-index: 1000;
border-radius: 0 0 0.5rem 0.5rem;
}
.skip-link:focus {
top: 0;
}
Alt Text Best Practices
- Informative images: Describe the content and function. "Bar chart showing Q3 revenue increased 15% over Q2."
- Decorative images: Use an empty alt (
alt="") so screen readers skip them. - Functional images (icons inside buttons): Describe the action, not the image. "Search" not "Magnifying glass icon."
- Complex images (charts, diagrams): Provide a brief alt and a longer description via
aria-describedbyor a linked data table.
<figure>
<img src="/img/revenue-chart.png"
alt="Bar chart: Q3 revenue $4.2M, up 15% from Q2"
aria-describedby="chart-desc" />
<figcaption id="chart-desc">
Quarterly revenue comparison. Q1: $3.1M, Q2: $3.7M, Q3: $4.2M.
</figcaption>
</figure>
Color Contrast
Use tools like the WebAIM Contrast Checker or browser DevTools to verify: - 4.5:1 ratio for normal text (<18pt or <14pt bold). - 3:1 ratio for large text (18pt+ or 14pt+ bold) and UI components.
Never rely on color alone to convey information. Pair color with text labels, patterns, or icons.
Screen Reader Testing
Automated tools catch roughly 30 percent of accessibility issues. Manual testing with screen readers is essential.
| Screen Reader | Platform | Browser |
|---|---|---|
| VoiceOver | macOS / iOS | Safari |
| NVDA | Windows | Firefox, Chrome |
| JAWS | Windows | Chrome, Edge |
| TalkBack | Android | Chrome |
Test the following flows: page load announcement, heading navigation (rotor/elements list), form completion, dynamic content updates, and modal interaction.
Automated Testing
Integrate automated accessibility checks into your CI pipeline:
// Using axe-core with Playwright
const { test, expect } = require('@playwright/test');
const AxeBuilder = require('@axe-core/playwright').default;
test('page has no accessibility violations', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21aa'])
.analyze();
expect(results.violations).toEqual([]);
});
# Lighthouse CLI accessibility audit
npx lighthouse https://example.com \
--only-categories=accessibility \
--output=json --output-path=./a11y-report.json
Automated testing is a safety net, not a replacement for human testing. Use it to catch regressions, then verify with real assistive technology.