Building Accessible Web Applications: A Practical Guide
Practical strategies for making your web applications accessible to all users, with code examples and testing techniques.

Web accessibility isn’t just about compliance—it’s about building applications that everyone can use. After years of building web applications, I’ve learned that accessibility is easiest when built in from the start.
Why Accessibility Matters
Consider these statistics:
- 1 in 4 adults in the US has some type of disability
- 8% of men have some form of color blindness
- Temporary impairments (broken arm, bright sunlight) affect everyone
Accessible websites benefit all users through better design, clearer content, and more intuitive interfaces.
Core Principles
1. Semantic HTML
Use the right element for the job:
<!-- Bad: Div soup -->
<div class="button" onclick="submit()">Submit</div>
<!-- Good: Semantic elements -->
<button type="submit">Submit</button>Semantic elements provide:
- Built-in keyboard support
- Screen reader announcements
- Focus management
- Form validation
2. Keyboard Navigation
Everything clickable should be keyboard accessible:
function handleKeyDown(event: KeyboardEvent) {
switch (event.key) {
case 'Enter':
case ' ':
event.preventDefault();
activateItem();
break;
case 'Escape':
closeModal();
break;
case 'ArrowDown':
focusNextItem();
break;
case 'ArrowUp':
focusPreviousItem();
break;
}
}3. Focus Management
Make focus visible and logical:
/* Never do this */
*:focus {
outline: none;
}
/* Do this instead */
:focus-visible {
outline: 2px solid var(--focus-color);
outline-offset: 2px;
}For modals and dialogs, trap focus appropriately:
function trapFocus(container: HTMLElement) {
const focusable = container.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const first = focusable[0] as HTMLElement;
const last = focusable[focusable.length - 1] as HTMLElement;
container.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();
}ARIA: Use Sparingly
ARIA is powerful but often misused. The first rule of ARIA:
Don’t use ARIA if you can use a native HTML element.
When ARIA is needed:
<!-- Custom dropdown -->
<div
role="combobox"
aria-expanded="false"
aria-haspopup="listbox"
aria-controls="options-list"
>
<input
type="text"
aria-autocomplete="list"
aria-controls="options-list"
/>
<ul
id="options-list"
role="listbox"
aria-label="Options"
>
<li role="option" aria-selected="false">Option 1</li>
<li role="option" aria-selected="true">Option 2</li>
</ul>
</div>Live Regions
Announce dynamic content changes:
<div aria-live="polite" aria-atomic="true" class="sr-only">
<!-- Content updates will be announced -->
</div>function announceMessage(message: string) {
const region = document.querySelector('[aria-live]');
region.textContent = message;
}
// Usage
announceMessage('Form submitted successfully');Color and Contrast
Minimum Contrast Ratios
- Normal text: 4.5:1
- Large text (18px+ or 14px+ bold): 3:1
- UI components: 3:1
Don’t Rely on Color Alone
<!-- Bad: Color only indicates error -->
<input class="border-red-500" />
<!-- Good: Multiple indicators -->
<input
class="border-red-500"
aria-invalid="true"
aria-describedby="error-msg"
/>
<p id="error-msg" class="text-red-500">
<span class="sr-only">Error:</span>
Email is required
</p>Motion and Animation
Respect user preferences:
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}const prefersReducedMotion = window.matchMedia(
'(prefers-reduced-motion: reduce)'
).matches;
if (!prefersReducedMotion) {
// Initialize complex animations
}Testing Accessibility
Automated Testing
# axe-core with Playwright
npm install @axe-core/playwright
# In your tests
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test('should not have accessibility violations', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});Manual Testing
- Keyboard only - Unplug your mouse, navigate everything
- Screen reader - VoiceOver (Mac), NVDA (Windows)
- Zoom - Test at 200% and 400%
- Color simulation - DevTools can simulate color blindness
Quick Wins
Start with these high-impact improvements:
- Add skip links for keyboard users
- Ensure all images have alt text
- Label all form inputs
- Make focus states visible
- Use headings in order (h1 → h2 → h3)
Resources
Building accessible applications is an ongoing process. Start where you are, make incremental improvements, and test with real users. The web should be for everyone.