Master web accessibility implementation to create inclusive web applications. Learn about semantic HTML, ARIA attributes, keyboard navigation, and testing practices. ๐
Semantic HTML ๐
<!-- Bad Example -->
<div class="header">
<div class="logo">Website Logo</div>
<div class="nav">
<div class="nav-item">Home</div>
<div class="nav-item">About</div>
</div>
</div>
<!-- Good Example -->
<header>
<img src="logo.png" alt="Website Logo">
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
</ul>
</nav>
</header>
<!-- Document Structure -->
<body>
<header>
<nav>
<!-- Navigation content -->
</nav>
</header>
<main>
<article>
<h1>Main Article Title</h1>
<section>
<h2>Section Title</h2>
<p>Section content...</p>
</section>
<aside>
<h3>Related Content</h3>
<!-- Related content -->
</aside>
</article>
</main>
<footer>
<!-- Footer content -->
</footer>
</body>
<!-- Form Example -->
<form>
<fieldset>
<legend>Personal Information</legend>
<div>
<label for="name">Full Name:</label>
<input
type="text"
id="name"
name="name"
required
aria-required="true"
>
</div>
<div>
<label for="email">Email:</label>
<input
type="email"
id="email"
name="email"
required
aria-required="true"
>
</div>
</fieldset>
<button type="submit">Submit</button>
</form>
ARIA Implementation ๐ฏ
<!-- ARIA Landmarks -->
<div role="banner">
<!-- Header content -->
</div>
<div role="navigation">
<!-- Navigation content -->
</div>
<div role="main">
<!-- Main content -->
</div>
<div role="complementary">
<!-- Sidebar content -->
</div>
<div role="contentinfo">
<!-- Footer content -->
</div>
<!-- ARIA States and Properties -->
<button
aria-expanded="false"
aria-controls="menu-content"
onclick="toggleMenu()"
>
Menu
</button>
<div
id="menu-content"
role="menu"
aria-hidden="true"
>
<!-- Menu items -->
</div>
<!-- Live Regions -->
<div
role="alert"
aria-live="assertive"
aria-atomic="true"
>
<!-- Dynamic content -->
</div>
<div
role="status"
aria-live="polite"
aria-atomic="true"
>
<!-- Status updates -->
</div>
<!-- Dialog Example -->
<div
role="dialog"
aria-labelledby="dialog-title"
aria-describedby="dialog-desc"
aria-modal="true"
>
<h2 id="dialog-title">Confirmation</h2>
<p id="dialog-desc">
Are you sure you want to proceed?
</p>
<button onclick="confirm()">Yes</button>
<button onclick="cancel()">No</button>
</div>
<!-- Tab Interface -->
<div role="tablist">
<button
role="tab"
aria-selected="true"
aria-controls="panel-1"
id="tab-1"
>
Tab 1
</button>
<button
role="tab"
aria-selected="false"
aria-controls="panel-2"
id="tab-2"
>
Tab 2
</button>
</div>
<div
role="tabpanel"
id="panel-1"
aria-labelledby="tab-1"
>
<!-- Panel 1 content -->
</div>
<div
role="tabpanel"
id="panel-2"
aria-labelledby="tab-2"
hidden
>
<!-- Panel 2 content -->
</div>
Keyboard Navigation โจ๏ธ
// Focus Management
function manageFocus() {
// Set initial focus
const firstInput = document.querySelector('input');
firstInput?.focus();
// Trap focus in modal
function trapFocus(element: HTMLElement) {
const focusableElements = element.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstFocusable = focusableElements[0];
const lastFocusable = focusableElements[
focusableElements.length - 1
];
function handleTabKey(e: KeyboardEvent) {
if (e.key !== 'Tab') return;
if (e.shiftKey) {
if (document.activeElement === firstFocusable) {
e.preventDefault();
lastFocusable.focus();
}
} else {
if (document.activeElement === lastFocusable) {
e.preventDefault();
firstFocusable.focus();
}
}
}
element.addEventListener('keydown', handleTabKey);
}
}
// Skip Links
const skipLink = document.createElement('a');
skipLink.href = '#main-content';
skipLink.className = 'skip-link';
skipLink.textContent = 'Skip to main content';
document.body.insertBefore(
skipLink,
document.body.firstChild
);
// Keyboard Shortcuts
class KeyboardShortcuts {
private shortcuts: Map<string, () => void>;
constructor() {
this.shortcuts = new Map();
this.handleKeyPress = this.handleKeyPress.bind(this);
}
register(key: string, callback: () => void) {
this.shortcuts.set(key.toLowerCase(), callback);
}
start() {
document.addEventListener(
'keydown',
this.handleKeyPress
);
}
stop() {
document.removeEventListener(
'keydown',
this.handleKeyPress
);
}
private handleKeyPress(event: KeyboardEvent) {
// Ignore if user is typing in an input
if (
event.target instanceof HTMLInputElement ||
event.target instanceof HTMLTextAreaElement
) {
return;
}
const key = event.key.toLowerCase();
const callback = this.shortcuts.get(key);
if (callback) {
event.preventDefault();
callback();
}
}
}
// Usage
const shortcuts = new KeyboardShortcuts();
shortcuts.register('/', () => {
// Focus search
document.querySelector('#search')?.focus();
});
shortcuts.register('h', () => {
// Go home
window.location.href = '/';
});
shortcuts.start();
Screen Reader Support ๐
// Announcements
class ScreenReaderAnnouncer {
private container: HTMLElement;
constructor() {
this.container = document.createElement('div');
this.container.setAttribute('role', 'status');
this.container.setAttribute('aria-live', 'polite');
this.container.setAttribute('aria-atomic', 'true');
// Hide visually but keep available to screen readers
this.container.style.position = 'absolute';
this.container.style.width = '1px';
this.container.style.height = '1px';
this.container.style.padding = '0';
this.container.style.margin = '-1px';
this.container.style.overflow = 'hidden';
this.container.style.clip = 'rect(0, 0, 0, 0)';
this.container.style.border = '0';
document.body.appendChild(this.container);
}
announce(message: string, priority: 'polite' | 'assertive' = 'polite') {
this.container.setAttribute('aria-live', priority);
// Clear previous content
this.container.textContent = '';
// Set new content in next tick
setTimeout(() => {
this.container.textContent = message;
}, 100);
}
}
// Usage
const announcer = new ScreenReaderAnnouncer();
function handleFormSubmit() {
// Process form...
announcer.announce('Form submitted successfully');
}
function handleError() {
announcer.announce(
'An error occurred. Please try again.',
'assertive'
);
}
// Description Lists
function createDescriptionList(
items: Array<{ term: string; description: string }>
) {
const dl = document.createElement('dl');
items.forEach(item => {
const dt = document.createElement('dt');
dt.textContent = item.term;
const dd = document.createElement('dd');
dd.textContent = item.description;
dl.appendChild(dt);
dl.appendChild(dd);
});
return dl;
}
// Table Accessibility
function createAccessibleTable(
headers: string[],
data: any[][]
) {
const table = document.createElement('table');
// Add caption
const caption = document.createElement('caption');
caption.textContent = 'Data Table';
table.appendChild(caption);
// Add header row
const thead = document.createElement('thead');
const headerRow = document.createElement('tr');
headers.forEach(header => {
const th = document.createElement('th');
th.setAttribute('scope', 'col');
th.textContent = header;
headerRow.appendChild(th);
});
thead.appendChild(headerRow);
table.appendChild(thead);
// Add data rows
const tbody = document.createElement('tbody');
data.forEach(row => {
const tr = document.createElement('tr');
row.forEach((cell, index) => {
const td = document.createElement('td');
td.setAttribute(
'headers',
headers[index].toLowerCase()
);
td.textContent = cell;
tr.appendChild(td);
});
tbody.appendChild(tr);
});
table.appendChild(tbody);
return table;
}
Testing ๐งช
// Accessibility Testing
import { axe } from 'jest-axe';
describe('Accessibility Tests', () => {
it('should have no accessibility violations', async () => {
const html = document.querySelector('#root');
const results = await axe(html);
expect(results).toHaveNoViolations();
});
});
// Focus Testing
describe('Focus Management', () => {
it('should trap focus in modal', () => {
const modal = document.querySelector('.modal');
const focusableElements = modal.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
// Test first to last
const firstElement = focusableElements[0];
const lastElement = focusableElements[
focusableElements.length - 1
];
firstElement.focus();
firstElement.dispatchEvent(
new KeyboardEvent('keydown', {
key: 'Tab',
shiftKey: true
})
);
expect(document.activeElement).toBe(lastElement);
// Test last to first
lastElement.dispatchEvent(
new KeyboardEvent('keydown', {
key: 'Tab'
})
);
expect(document.activeElement).toBe(firstElement);
});
});
// Screen Reader Testing
describe('Screen Reader Announcements', () => {
it('should announce form submission', () => {
const announcer = document.querySelector(
'[aria-live="polite"]'
);
// Submit form
handleFormSubmit();
// Wait for announcement
setTimeout(() => {
expect(announcer.textContent).toBe(
'Form submitted successfully'
);
}, 100);
});
});
Best Practices ๐
- Use semantic HTML
- Implement proper ARIA
- Support keyboard navigation
- Provide screen reader support
- Implement proper testing
- Use proper color contrast
- Provide text alternatives
- Support zoom and scaling
- Implement proper focus
- Follow WCAG guidelines
Additional Resources
Web accessibility is crucial for creating inclusive web applications. This guide covers essential practices and patterns for implementing accessibility features in web applications.