Web Accessibility (a11y)
WCAG 2.2, ARIA roles, keyboard navigation, screen readers, focus management.
a11yaccessibilityfrontendhtml
# Web Accessibility (a11y)
## WCAG 2.2 principles (POUR)
- **Perceivable**: content visible/audible to all senses
- **Operable**: navigable by keyboard, no seizure triggers
- **Understandable**: readable, predictable behavior
- **Robust**: works with assistive technologies
## Semantic HTML (first line of defense)
```html
<!-- Use elements for their meaning -->
<header>, <nav>, <main>, <aside>, <footer>
<article>, <section>, <figure>, <figcaption>
<button> (not <div onclick>)
<a href> (not <span onclick>)
<ul>/<ol> for lists, <table> for tabular data
```
## ARIA
```html
<!-- Only add ARIA when semantic HTML isn't enough -->
<div role="dialog" aria-modal="true" aria-labelledby="title">
<button aria-expanded="false" aria-controls="menu">
<div role="alert" aria-live="polite">Error message</div>
<img alt="Descriptive text" /> <!-- empty alt="" for decorative -->
<!-- Progress/status -->
<div role="progressbar" aria-valuenow="50" aria-valuemin="0" aria-valuemax="100">
<!-- Required/invalid -->
<input aria-required="true" aria-invalid="true" aria-describedby="err">
<span id="err">Email is required</span>
```
## Keyboard navigation
- Every interactive element must be reachable via Tab.
- Focus order must be logical (match visual order).
- Provide visible focus indicator (never `outline: none` without replacement).
- Keyboard shortcuts: Enter/Space = activate, Esc = close, Arrow keys = navigate within widget.
```css
/* Visible focus */
:focus-visible {
outline: 2px solid #0066cc;
outline-offset: 2px;
}
```
## Focus management
```js
// Move focus to modal on open
dialog.addEventListener('open', () => firstFocusable.focus())
// Trap focus inside modal
document.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()
}
}
})
// Return focus on close
const trigger = document.activeElement
dialog.close()
trigger.focus()
```
## Color & contrast
- Normal text: 4.5:1 contrast ratio (WCAG AA)
- Large text (18px+ or 14px bold): 3:1
- Non-text (icons, borders): 3:1
- Never convey information by color alone
## Testing
- Browser: axe DevTools, Wave, Lighthouse accessibility audit
- Keyboard: tab through entire page manually
- Screen reader: NVDA (Windows), VoiceOver (Mac/iOS), TalkBack (Android)
- `eslint-plugin-jsx-a11y` for ReactAPI: /api/skills/web-accessibility