Accessibility is not a feature — it is a quality attribute, like performance or security. You do not ship a “performance version” of your app. You ship an app that is fast. The same applies to accessibility: you ship an app that everyone can use, including the 15% of the global population living with some form of disability.
Beyond the moral imperative, accessibility is a legal requirement in many jurisdictions (ADA, EAA, Section 508) and a business advantage — accessible interfaces are better for everyone, not just users with disabilities.
The Four Principles (POUR)
| Principle | What It Means | Example |
|---|
| Perceivable | Users can perceive the content | Alt text on images, captions on video, sufficient color contrast |
| Operable | Users can interact with the interface | Keyboard navigation, no time limits, no seizure-inducing animations |
| Understandable | Users can understand the content | Clear language, consistent navigation, error prevention |
| Robust | Content works with assistive technologies | Semantic HTML, valid ARIA, tested with screen readers |
WCAG Compliance Levels
| Level | Requirement | Target |
|---|
| A | Minimum accessibility | Legal minimum in most jurisdictions |
| AA | Standard accessibility | Target this. Most regulations require AA. |
| AAA | Enhanced accessibility | Aspirational. Not required but beneficial. |
Key WCAG AA Requirements
| Requirement | Criterion | What to Do |
|---|
| Color contrast (text) | 4.5:1 for normal text, 3:1 for large text | Use contrast checker tools |
| Color contrast (UI) | 3:1 for interactive elements | Buttons, form fields, focus indicators |
| Keyboard navigation | All functionality via keyboard | Tab order, focus management, no keyboard traps |
| Alt text | All images have descriptive alternatives | Meaningful alt text, empty alt for decorative |
| Form labels | Every input has an associated label | <label for="email"> or aria-label |
| Error identification | Errors are described in text | Not just red border — text explanation |
| Resize | Content usable at 200% zoom | Responsive design, no horizontal scroll |
| Focus visible | Keyboard focus is always visible | Custom focus styles, never outline: none |
Semantic HTML: The Foundation
<!-- ❌ Div soup: no semantic meaning, invisible to screen readers -->
<div class="header">
<div class="nav">
<div class="nav-item" onclick="navigate('/home')">Home</div>
<div class="nav-item" onclick="navigate('/about')">About</div>
</div>
</div>
<div class="main">
<div class="article">
<div class="title">How to Build Accessible Forms</div>
<div class="content">...</div>
</div>
</div>
<!-- ✅ Semantic HTML: meaningful structure, works with assistive tech -->
<header>
<nav aria-label="Main navigation">
<ul>
<li><a href="/home">Home</a></li>
<li><a href="/about">About</a></li>
</ul>
</nav>
</header>
<main>
<article>
<h1>How to Build Accessible Forms</h1>
<p>...</p>
</article>
</main>
Landmark Roles
| Element | Role | Purpose |
|---|
<header> | banner | Site-wide header |
<nav> | navigation | Navigation links |
<main> | main | Primary content |
<aside> | complementary | Related content |
<footer> | contentinfo | Site-wide footer |
<form> | form | User input |
<section> | region (with aria-label) | Thematic grouping |
Keyboard Navigation
Essential keyboard interactions:
Tab → Move to next focusable element
Shift+Tab → Move to previous focusable element
Enter → Activate links, buttons, submit forms
Space → Activate buttons, toggle checkboxes
Arrow keys → Navigate within components (tabs, menus, radio groups)
Escape → Close modals, menus, popups
Focus Management
// ✅ Managing focus when opening a modal
function openModal(modalElement) {
const previousFocus = document.activeElement;
modalElement.hidden = false;
modalElement.setAttribute('aria-modal', 'true');
// Move focus to first focusable element in modal
const firstFocusable = modalElement.querySelector(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
firstFocusable?.focus();
// Trap focus within modal
modalElement.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeModal(modalElement, previousFocus);
}
trapFocus(e, modalElement);
});
}
function closeModal(modalElement, previousFocus) {
modalElement.hidden = true;
modalElement.removeAttribute('aria-modal');
previousFocus?.focus(); // Return focus to trigger element
}
ARIA: When HTML Is Not Enough
First rule of ARIA: Do not use ARIA if native HTML can do the job.
<!-- ❌ ARIA that duplicates native behavior -->
<div role="button" tabindex="0" aria-label="Submit" onclick="submit()">
Submit
</div>
<!-- ✅ Native HTML — inherently accessible -->
<button type="submit">Submit</button>
When ARIA Is Necessary
<!-- Custom tab component — no native HTML equivalent -->
<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 settings content...
</div>
<div role="tabpanel" id="panel-security" aria-labelledby="tab-security"
hidden>
Security settings content...
</div>
<!-- Live regions for dynamic content -->
<div aria-live="polite" aria-atomic="true">
<!-- Screen reader announces when content changes -->
<p>3 items in your cart</p>
</div>
<!-- Loading states -->
<div aria-busy="true" aria-label="Loading search results">
<span class="spinner"></span>
</div>
Automated Testing
Automated tools catch approximately 30% of accessibility issues. The rest require manual testing with keyboard navigation and screen readers.
// jest-axe: automated accessibility testing in unit tests
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);
test('login form has no accessibility violations', async () => {
const { container } = render(<LoginForm />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
| Tool | Type | What It Catches |
|---|
| axe-core | Automated (unit/integration) | Missing alt text, contrast, ARIA errors |
| Lighthouse | Automated (browser) | WCAG A/AA violations |
| Pa11y | CI pipeline | Automated WCAG testing per page |
| Keyboard testing | Manual | Focus traps, missing interactions |
| Screen reader (VoiceOver, NVDA) | Manual | Content order, announcements, labels |
Testing Checklist (Manual)
| Test | How | Pass Criteria |
|---|
| Tab through entire page | Press Tab repeatedly | Every interactive element receives focus in logical order |
| Activate every control | Enter/Space on buttons, links | All actions work without mouse |
| Check focus visibility | Tab through and observe | Focus indicator is always visible |
| Use screen reader | VoiceOver / NVDA | All content is announced, labels make sense |
| Zoom to 200% | Browser zoom | No content is cut off, no horizontal scrolling |
| Disable CSS | Browser dev tools | Content is still readable in logical order |
Implementation Checklist