All checks were successful
Build and Deploy Nuxt / build (push) Successful in 30s
441 lines
12 KiB
Markdown
441 lines
12 KiB
Markdown
---
|
||
name: accessibility
|
||
description: Audit and improve web accessibility following WCAG 2.2 guidelines. Use when asked to "improve accessibility", "a11y audit", "WCAG compliance", "screen reader support", "keyboard navigation", or "make accessible".
|
||
license: MIT
|
||
metadata:
|
||
author: web-quality-skills
|
||
version: "1.1"
|
||
---
|
||
|
||
# Accessibility (a11y)
|
||
|
||
Comprehensive accessibility guidelines based on WCAG 2.2 and Lighthouse accessibility audits. Goal: make content usable by everyone, including people with disabilities.
|
||
|
||
## WCAG Principles: POUR
|
||
|
||
| Principle | Description |
|
||
|-----------|-------------|
|
||
| **P**erceivable | Content can be perceived through different senses |
|
||
| **O**perable | Interface can be operated by all users |
|
||
| **U**nderstandable | Content and interface are understandable |
|
||
| **R**obust | Content works with assistive technologies |
|
||
|
||
## Conformance levels
|
||
|
||
| Level | Requirement | Target |
|
||
|-------|-------------|--------|
|
||
| **A** | Minimum accessibility | Must pass |
|
||
| **AA** | Standard compliance | Should pass (legal requirement in many jurisdictions) |
|
||
| **AAA** | Enhanced accessibility | Nice to have |
|
||
|
||
---
|
||
|
||
## Perceivable
|
||
|
||
### Text alternatives (1.1)
|
||
|
||
**Images require alt text:**
|
||
```html
|
||
<!-- ❌ Missing alt -->
|
||
<img src="chart.png">
|
||
|
||
<!-- ✅ Descriptive alt -->
|
||
<img src="chart.png" alt="Bar chart showing 40% increase in Q3 sales">
|
||
|
||
<!-- ✅ Decorative image (empty alt) -->
|
||
<img src="decorative-border.png" alt="" role="presentation">
|
||
|
||
<!-- ✅ Complex image with longer description -->
|
||
<figure>
|
||
<img src="infographic.png" alt="2024 market trends infographic"
|
||
aria-describedby="infographic-desc">
|
||
<figcaption id="infographic-desc">
|
||
<!-- Detailed description -->
|
||
</figcaption>
|
||
</figure>
|
||
```
|
||
|
||
**Icon buttons need accessible names:**
|
||
```html
|
||
<!-- ❌ No accessible name -->
|
||
<button><svg><!-- menu icon --></svg></button>
|
||
|
||
<!-- ✅ Using aria-label -->
|
||
<button aria-label="Open menu">
|
||
<svg aria-hidden="true"><!-- menu icon --></svg>
|
||
</button>
|
||
|
||
<!-- ✅ Using visually hidden text -->
|
||
<button>
|
||
<svg aria-hidden="true"><!-- menu icon --></svg>
|
||
<span class="visually-hidden">Open menu</span>
|
||
</button>
|
||
```
|
||
|
||
**Visually hidden class:**
|
||
```css
|
||
.visually-hidden {
|
||
position: absolute;
|
||
width: 1px;
|
||
height: 1px;
|
||
padding: 0;
|
||
margin: -1px;
|
||
overflow: hidden;
|
||
clip: rect(0, 0, 0, 0);
|
||
white-space: nowrap;
|
||
border: 0;
|
||
}
|
||
```
|
||
|
||
### Color contrast (1.4.3, 1.4.6)
|
||
|
||
| Text Size | AA minimum | AAA enhanced |
|
||
|-----------|------------|--------------|
|
||
| Normal text (< 18px / < 14px bold) | 4.5:1 | 7:1 |
|
||
| Large text (≥ 18px / ≥ 14px bold) | 3:1 | 4.5:1 |
|
||
| UI components & graphics | 3:1 | 3:1 |
|
||
|
||
```css
|
||
/* ❌ Low contrast (2.5:1) */
|
||
.low-contrast {
|
||
color: #999;
|
||
background: #fff;
|
||
}
|
||
|
||
/* ✅ Sufficient contrast (7:1) */
|
||
.high-contrast {
|
||
color: #333;
|
||
background: #fff;
|
||
}
|
||
|
||
/* ✅ Focus states need contrast too */
|
||
:focus-visible {
|
||
outline: 2px solid #005fcc;
|
||
outline-offset: 2px;
|
||
}
|
||
```
|
||
|
||
**Don't rely on color alone:**
|
||
```html
|
||
<!-- ❌ Only color indicates error -->
|
||
<input class="error-border">
|
||
<style>.error-border { border-color: red; }</style>
|
||
|
||
<!-- ✅ Color + icon + text -->
|
||
<div class="field-error">
|
||
<input aria-invalid="true" aria-describedby="email-error">
|
||
<span id="email-error" class="error-message">
|
||
<svg aria-hidden="true"><!-- error icon --></svg>
|
||
Please enter a valid email address
|
||
</span>
|
||
</div>
|
||
```
|
||
|
||
### Media alternatives (1.2)
|
||
|
||
```html
|
||
<!-- Video with captions -->
|
||
<video controls>
|
||
<source src="video.mp4" type="video/mp4">
|
||
<track kind="captions" src="captions.vtt" srclang="en" label="English" default>
|
||
<track kind="descriptions" src="descriptions.vtt" srclang="en" label="Descriptions">
|
||
</video>
|
||
|
||
<!-- Audio with transcript -->
|
||
<audio controls>
|
||
<source src="podcast.mp3" type="audio/mp3">
|
||
</audio>
|
||
<details>
|
||
<summary>Transcript</summary>
|
||
<p>Full transcript text...</p>
|
||
</details>
|
||
```
|
||
|
||
---
|
||
|
||
## Operable
|
||
|
||
### Keyboard accessible (2.1)
|
||
|
||
**All functionality must be keyboard accessible:**
|
||
```javascript
|
||
// ❌ Only handles click
|
||
element.addEventListener('click', handleAction);
|
||
|
||
// ✅ Handles both click and keyboard
|
||
element.addEventListener('click', handleAction);
|
||
element.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Enter' || e.key === ' ') {
|
||
e.preventDefault();
|
||
handleAction();
|
||
}
|
||
});
|
||
```
|
||
|
||
**No keyboard traps.** Users must be able to Tab into and out of every component. Use the [modal focus trap pattern](references/A11Y-PATTERNS.md#modal-focus-trap) for dialogs—the native `<dialog>` element handles this automatically.
|
||
|
||
### Focus visible (2.4.7)
|
||
|
||
```css
|
||
/* ❌ Never remove focus outlines */
|
||
*:focus { outline: none; }
|
||
|
||
/* ✅ Use :focus-visible for keyboard-only focus */
|
||
:focus {
|
||
outline: none;
|
||
}
|
||
|
||
:focus-visible {
|
||
outline: 2px solid #005fcc;
|
||
outline-offset: 2px;
|
||
}
|
||
|
||
/* ✅ Or custom focus styles */
|
||
button:focus-visible {
|
||
box-shadow: 0 0 0 3px rgba(0, 95, 204, 0.5);
|
||
}
|
||
```
|
||
|
||
### Focus not obscured (2.4.11) — new in 2.2
|
||
|
||
When an element receives keyboard focus, it must not be entirely hidden by other author-created content such as sticky headers, footers, or overlapping panels. At Level AAA (2.4.12), no part of the focused element may be hidden.
|
||
|
||
```css
|
||
/* ✅ Account for sticky headers when scrolling to focused elements */
|
||
:target {
|
||
scroll-margin-top: 80px;
|
||
}
|
||
|
||
/* ✅ Ensure focused items clear fixed/sticky bars */
|
||
:focus {
|
||
scroll-margin-top: 80px;
|
||
scroll-margin-bottom: 60px;
|
||
}
|
||
```
|
||
|
||
### Skip links (2.4.1)
|
||
|
||
Provide a skip link so keyboard users can bypass repetitive navigation. See the [skip link pattern](references/A11Y-PATTERNS.md#skip-link) for full markup and styles.
|
||
|
||
### Target size (2.5.8) — new in 2.2
|
||
|
||
Interactive targets must be at least **24 × 24 CSS pixels** (AA). Exceptions: inline text links, elements where the browser controls the size, and targets where a 24px circle centered on the bounding box does not overlap another target.
|
||
|
||
```css
|
||
/* ✅ Minimum target size */
|
||
button,
|
||
[role="button"],
|
||
input[type="checkbox"] + label,
|
||
input[type="radio"] + label {
|
||
min-width: 24px;
|
||
min-height: 24px;
|
||
}
|
||
|
||
/* ✅ Comfortable target size (recommended 44×44) */
|
||
.touch-target {
|
||
min-width: 44px;
|
||
min-height: 44px;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
```
|
||
|
||
### Dragging movements (2.5.7) — new in 2.2
|
||
|
||
Any action that requires dragging must have a single-pointer alternative (e.g., buttons, inputs). See the [dragging movements pattern](references/A11Y-PATTERNS.md#dragging-movements) for a sortable-list example.
|
||
|
||
### Timing (2.2)
|
||
|
||
```javascript
|
||
// Allow users to extend time limits
|
||
function showSessionWarning() {
|
||
const modal = createModal({
|
||
title: 'Session Expiring',
|
||
content: 'Your session will expire in 2 minutes.',
|
||
actions: [
|
||
{ label: 'Extend session', action: extendSession },
|
||
{ label: 'Log out', action: logout }
|
||
],
|
||
timeout: 120000
|
||
});
|
||
}
|
||
```
|
||
|
||
### Motion (2.3)
|
||
|
||
```css
|
||
/* Respect reduced motion preference */
|
||
@media (prefers-reduced-motion: reduce) {
|
||
*,
|
||
*::before,
|
||
*::after {
|
||
animation-duration: 0.01ms !important;
|
||
animation-iteration-count: 1 !important;
|
||
transition-duration: 0.01ms !important;
|
||
scroll-behavior: auto !important;
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Understandable
|
||
|
||
### Page language (3.1.1)
|
||
|
||
```html
|
||
<!-- ❌ No language specified -->
|
||
<html>
|
||
|
||
<!-- ✅ Language specified -->
|
||
<html lang="en">
|
||
|
||
<!-- ✅ Language changes within page -->
|
||
<p>The French word for hello is <span lang="fr">bonjour</span>.</p>
|
||
```
|
||
|
||
### Consistent navigation (3.2.3)
|
||
|
||
```html
|
||
<!-- Navigation should be consistent across pages -->
|
||
<nav aria-label="Main">
|
||
<ul>
|
||
<li><a href="/" aria-current="page">Home</a></li>
|
||
<li><a href="/products">Products</a></li>
|
||
<li><a href="/about">About</a></li>
|
||
</ul>
|
||
</nav>
|
||
```
|
||
|
||
### Consistent help (3.2.6) — new in 2.2
|
||
|
||
If a help mechanism (contact info, chat widget, FAQ link, self-help option) is repeated across multiple pages, it must appear in the **same relative order** each time. Users who rely on consistent placement shouldn't have to hunt for help on every page.
|
||
|
||
### Form labels (3.3.2)
|
||
|
||
Every input needs a programmatically associated label. See the [form labels pattern](references/A11Y-PATTERNS.md#form-labels) for explicit, implicit, and instructional examples.
|
||
|
||
### Error handling (3.3.1, 3.3.3)
|
||
|
||
Announce errors to screen readers with `role="alert"` or `aria-live`, set `aria-invalid="true"` on invalid fields, and focus the first error on submit. See the [error handling pattern](references/A11Y-PATTERNS.md#error-handling) for full markup and JS.
|
||
|
||
### Redundant entry (3.3.7) — new in 2.2
|
||
|
||
Don't force users to re-enter information they already provided in the same session. Auto-populate from earlier steps, or let users select from previously entered values. Exceptions: security re-confirmation and content that has expired.
|
||
|
||
```html
|
||
<!-- ✅ Auto-fill shipping address from billing -->
|
||
<fieldset>
|
||
<legend>Shipping address</legend>
|
||
<label>
|
||
<input type="checkbox" id="same-as-billing" checked>
|
||
Same as billing address
|
||
</label>
|
||
<!-- Fields auto-populated when checked -->
|
||
</fieldset>
|
||
```
|
||
|
||
### Accessible authentication (3.3.8) — new in 2.2
|
||
|
||
Login flows must not rely on cognitive function tests (e.g., remembering a password, solving a puzzle) unless at least one of:
|
||
- A copy-paste or autofill mechanism is available
|
||
- An alternative method exists (e.g., passkey, SSO, email link)
|
||
- The test uses object recognition or personal content (AA only; AAA removes this exception)
|
||
|
||
```html
|
||
<!-- ✅ Allow paste in password fields -->
|
||
<input type="password" id="password" autocomplete="current-password">
|
||
|
||
<!-- ✅ Offer passwordless alternatives -->
|
||
<button type="button">Sign in with passkey</button>
|
||
<button type="button">Email me a login link</button>
|
||
```
|
||
|
||
---
|
||
|
||
## Robust
|
||
|
||
### ARIA usage (4.1.2)
|
||
|
||
**Prefer native elements:**
|
||
```html
|
||
<!-- ❌ ARIA role on div -->
|
||
<div role="button" tabindex="0">Click me</div>
|
||
|
||
<!-- ✅ Native button -->
|
||
<button>Click me</button>
|
||
|
||
<!-- ❌ ARIA checkbox -->
|
||
<div role="checkbox" aria-checked="false">Option</div>
|
||
|
||
<!-- ✅ Native checkbox -->
|
||
<label><input type="checkbox"> Option</label>
|
||
```
|
||
|
||
**When ARIA is needed,** use the correct roles and states. See the [ARIA tabs pattern](references/A11Y-PATTERNS.md#aria-tabs) for a complete tablist example.
|
||
|
||
### Live regions (4.1.3)
|
||
|
||
Use `aria-live` regions to announce dynamic content changes without moving focus. See the [live regions pattern](references/A11Y-PATTERNS.md#live-regions-and-notifications) for markup and a `showNotification()` helper.
|
||
|
||
---
|
||
|
||
## Testing checklist
|
||
|
||
### Automated testing
|
||
```bash
|
||
# Lighthouse accessibility audit
|
||
npx lighthouse https://example.com --only-categories=accessibility
|
||
|
||
# axe-core
|
||
npm install @axe-core/cli -g
|
||
axe https://example.com
|
||
```
|
||
|
||
### Manual testing
|
||
|
||
- [ ] **Keyboard navigation:** Tab through entire page, use Enter/Space to activate
|
||
- [ ] **Screen reader:** Test with VoiceOver (Mac), NVDA (Windows), or TalkBack (Android)
|
||
- [ ] **Zoom:** Content usable at 200% zoom
|
||
- [ ] **High contrast:** Test with Windows High Contrast Mode
|
||
- [ ] **Reduced motion:** Test with `prefers-reduced-motion: reduce`
|
||
- [ ] **Focus order:** Logical and follows visual order
|
||
- [ ] **Target size:** Interactive elements meet 24×24px minimum
|
||
|
||
See the [screen reader commands reference](references/A11Y-PATTERNS.md#screen-reader-commands) for VoiceOver and NVDA shortcuts.
|
||
|
||
---
|
||
|
||
## Common issues by impact
|
||
|
||
### Critical (fix immediately)
|
||
1. Missing form labels
|
||
2. Missing image alt text
|
||
3. Insufficient color contrast
|
||
4. Keyboard traps
|
||
5. No focus indicators
|
||
|
||
### Serious (fix before launch)
|
||
1. Missing page language
|
||
2. Missing heading structure
|
||
3. Non-descriptive link text
|
||
4. Auto-playing media
|
||
5. Missing skip links
|
||
|
||
### Moderate (fix soon)
|
||
1. Missing ARIA labels on icons
|
||
2. Inconsistent navigation
|
||
3. Missing error identification
|
||
4. Timing without controls
|
||
5. Missing landmark regions
|
||
|
||
## References
|
||
|
||
- [WCAG 2.2 Quick Reference](https://www.w3.org/WAI/WCAG22/quickref/)
|
||
- [WAI-ARIA Authoring Practices](https://www.w3.org/WAI/ARIA/apg/)
|
||
- [Deque axe Rules](https://dequeuniversity.com/rules/axe/)
|
||
- [Web Quality Audit](../web-quality-audit/SKILL.md)
|
||
- [WCAG criteria reference](references/WCAG.md)
|
||
- [Accessibility code patterns](references/A11Y-PATTERNS.md)
|