Accessibility Engineering
Build accessible web applications that work for everyone. Covers WCAG compliance, semantic HTML, ARIA patterns, keyboard navigation, screen reader testing, and making accessibility a first-class engineering concern.
Accessibility is not a feature — it is a quality attribute of every feature. 15% of the world’s population lives with some form of disability. Beyond ethics and inclusion, accessibility failures create legal liability (ADA lawsuits increased 300% since 2018) and exclude paying customers.
WCAG Compliance Levels
| Level | Requirement | Examples |
|---|---|---|
| A (minimum) | Content is accessible | Text alternatives, keyboard navigation |
| AA (standard target) | Usable by most people | Color contrast 4.5:1, focus indicators |
| AAA (enhanced) | Optimized for accessibility | Color contrast 7:1, extended audio descriptions |
Target WCAG 2.1 Level AA — this is the legal standard in most jurisdictions and the right balance of effort vs. coverage.
Semantic HTML
The most impactful accessibility improvement is using the correct HTML elements:
<!-- BAD: Divs with click handlers -->
<div class="button" onclick="submitForm()">Submit</div>
<div class="nav">
<div class="nav-item" onclick="goHome()">Home</div>
</div>
<!-- GOOD: Semantic elements -->
<button type="submit">Submit</button>
<nav aria-label="Main navigation">
<a href="/">Home</a>
</nav>
Semantic HTML gives you keyboard navigation, screen reader support, and focus management for free.
Heading Hierarchy
<!-- CORRECT hierarchy -->
<h1>Product Catalog</h1>
<h2>Electronics</h2>
<h3>Laptops</h3>
<h3>Phones</h3>
<h2>Clothing</h2>
<h3>Men's</h3>
<h3>Women's</h3>
<!-- WRONG: Skipping levels, multiple h1s -->
<h1>Product Catalog</h1>
<h3>Electronics</h3> <!-- Skipped h2 -->
<h1>Categories</h1> <!-- Multiple h1s -->
ARIA Patterns
Use ARIA only when native HTML cannot express the UI pattern:
Live Regions
<!-- Announce dynamic content changes to screen readers -->
<div role="status" aria-live="polite">
3 results found
</div>
<div role="alert" aria-live="assertive">
Error: Payment failed. Please try again.
</div>
Tab Panel
<div role="tablist" aria-label="Account Settings">
<button role="tab" aria-selected="true" aria-controls="panel-profile"
id="tab-profile">Profile</button>
<button role="tab" aria-selected="false" aria-controls="panel-security"
id="tab-security" tabindex="-1">Security</button>
</div>
<div role="tabpanel" id="panel-profile" aria-labelledby="tab-profile">
Profile content...
</div>
<div role="tabpanel" id="panel-security" aria-labelledby="tab-security"
hidden>
Security content...
</div>
Keyboard Navigation
Every interactive element must be reachable and operable via keyboard:
// Focus trap for modals
function trapFocus(modal) {
const focusableElements = modal.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const first = focusableElements[0];
const last = focusableElements[focusableElements.length - 1];
modal.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
if (e.key === 'Escape') closeModal();
});
first.focus();
}
Focus Indicators
/* Never remove focus outlines without replacement */
/* BAD */
*:focus { outline: none; }
/* GOOD: Custom focus indicator */
:focus-visible {
outline: 3px solid var(--color-focus);
outline-offset: 2px;
border-radius: 2px;
}
Color and Contrast
/* WCAG AA: 4.5:1 contrast ratio for normal text */
/* WCAG AA: 3:1 contrast ratio for large text (18px+ or 14px+ bold) */
/* Check: https://webaim.org/resources/contrastchecker/ */
/* Never use color alone to convey information */
/* BAD */
.error { color: red; }
/* GOOD: Color + icon + text */
.error {
color: var(--color-error);
border-left: 4px solid var(--color-error);
}
.error::before { content: "⚠ "; }
Testing
Automated
# axe-core in CI
npx @axe-core/cli https://localhost:3000
# Lighthouse accessibility audit
npx lighthouse https://localhost:3000 --only-categories=accessibility
# jest-axe for component tests
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);
test('button has no accessibility violations', async () => {
const { container } = render(<Button>Click me</Button>);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
Manual
Screen readers: VoiceOver (Mac), NVDA (Windows), TalkBack (Android)
Keyboard only: Navigate entire app without mouse
Zoom: 200% zoom, no horizontal scrolling
Color: Grayscale mode, high contrast mode
Motion: prefers-reduced-motion respected
Anti-Patterns
| Anti-Pattern | Consequence | Fix |
|---|---|---|
div and span for everything | No keyboard/screen reader support | Use semantic HTML elements |
| Focus outlines removed | Keyboard users cannot see focus | Custom :focus-visible styles |
| Images without alt text | Screen readers say “image” with no context | Descriptive alt text (or alt="" for decorative) |
| Color-only indicators | Colorblind users miss information | Use color + icon + text |
| Accessibility as an afterthought | Retrofitting is 10x more expensive | Build accessible from the start |
Accessibility is a technical requirement, not a checkbox. Build it into your component library, your review process, and your testing pipeline.