
Web Accessibility Audit
- 196 installs
- 818 repo stars
- Updated May 12, 2026
- warpdotdev/oz-skills
Use web-accessibility-audit for development tasks
About
web-accessibility-audit: A skill skill for development. This skill provides functionality for development workflows.
- web-accessibility-audit
Web Accessibility Audit by the numbers
- 196 all-time installs (skills.sh)
- +3 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #2,033 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/warpdotdev/oz-skills --skill web-accessibility-auditAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 196 |
|---|---|
| repo stars | ★ 818 |
| Last updated | May 12, 2026 |
| Repository | warpdotdev/oz-skills ↗ |
What it does
Use web-accessibility-audit for development tasks
Files
Accessibility Auditor
Audit web applications for WCAG 2.0/2.1/2.2 compliance by identifying common violations and providing actionable remediation steps.
When to Use
- User requests accessibility audit, a11y check, or WCAG compliance review
- User mentions accessibility issues, screen readers, or keyboard navigation problems
- User asks to check or improve accessibility for people with disabilities
WCAG Principles: POUR
| Principle | Description |
|---|---|
| Perceivable | Content can be perceived through different senses |
| Operable | Interface can be operated by all users |
| Understandable | Content and interface are understandable |
| Robust | 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 |
---
12 Most Common WCAG Violations
Based on WebAIM Million (2021) research analyzing top 1M websites:
1. Low Color Contrast (WCAG 1.4.3) - 86.4% of sites
- Text < 4.5:1 contrast ratio
- Large text < 3:1 contrast ratio
- UI components < 3:1
2. Missing/Inadequate Alt Text (WCAG 1.1.1) - 60.6% of sites
- Images without alt attribute
- Alt text with "image", "picture", "photo"
- Empty alt on meaningful images
3. Missing Name, Role, or Value (WCAG 4.1.2)
- Interactive elements without accessible names
- Custom components without proper ARIA
- Buttons, form fields, custom widgets
4. Keyboard Navigation Failures (WCAG 2.1.1)
- Elements with onClick but not keyboard accessible
- Missing focus indicators
- Trapped keyboard focus
5. Unlabeled Form Controls (WCAG 1.3.1, 3.3.2) - 39.6% of sites
- Inputs without
<label>or aria-label - Labels not programmatically associated
6. Missing Language Attributes (WCAG 3.1.1) - 28.9% of sites
- No lang attribute on
<html> - Missing lang for foreign language passages
7. Improper Heading Structure (WCAG 1.3.1, 2.4.6)
- Skipped heading levels (h1 → h3)
- Multiple h1s or no h1
- Empty headings
8. Empty Links or Poor Link Text (WCAG 2.4.4)
- Links with "click here", "here", "read more"
- Empty links or links with only icons
9. Missing/Improper Focus Indicators (WCAG 2.4.7)
- CSS removing outline without replacement
- Insufficient focus indicator contrast
10. Overuse/Misuse of ARIA (WCAG 4.1.2)
- Unnecessary ARIA when native HTML works
- Invalid ARIA attributes for roles
- Required ARIA attributes missing
11. Inadequate Data Table Markup (WCAG 1.3.1)
- Tables without
<th>elements - Missing scope or headers attributes
12. Missing Media Captions (WCAG 1.2.1, 1.2.2)
- Videos without captions/subtitles
- Audio without transcripts
---
Audit Process
Phase 1: Automated Testing
Run ESLint (React/JSX projects):
npx eslint --ext .jsx,.tsx --no-ignore --format json . > .claude/skills/a11y-auditor/eslint-results.json 2>&1 || trueOr use helper script: .claude/skills/a11y-auditor/scripts/run-eslint.sh
Run Lighthouse (production/staging):
npx lighthouse https://example.com --only-categories=accessibility --output=json --output-path=./lighthouse-results.jsonCheck for axe-core integration:
grep -r "@axe-core\|axe-core" package.jsonPhase 2: Manual Code Inspection
Use grep patterns from references/grep-patterns.md to search for:
- Missing alt text
- Keyboard navigation issues
- Color values for contrast checking
- ARIA issues
- Form labels
- Heading structure
- Language attributes
- Poor link text
- Media elements
See references/grep-patterns.md for complete pattern list.
Phase 3: Analyze & Prioritize
Group findings by severity using WCAG impact levels:
Critical (fix immediately):
- Keyboard traps
- No focus indicators
- Missing form labels
- Missing alt text on functional images
- Insufficient color contrast on interactive elements
Serious (fix before launch):
- Missing page language
- Improper heading structure
- Non-descriptive link text
- Missing skip links
- Auto-playing media
Moderate (fix soon):
- Missing ARIA labels on icons
- Inconsistent navigation
- Missing error identification
- Missing landmark regions
Phase 4: Manual Testing
Follow references/screen-reader-guide.md for:
- Keyboard navigation testing
- Screen reader testing (VoiceOver, NVDA, JAWS)
- Zoom and reflow testing
- High contrast mode testing
- Reduced motion testing
---
WCAG Pattern Examples
Perceivable
Alt Text (1.1.1)
<!-- ❌ Missing alt -->
<img src="chart.png">
<!-- ✅ Descriptive alt -->
<img src="chart.png" alt="Bar chart showing 40% increase in Q3 sales">
<!-- ✅ Decorative (empty alt) -->
<img src="decorative-border.png" alt="" role="presentation">Color Contrast (1.4.3)
/* ❌ Low contrast (2.5:1) */
.low-contrast {
color: #999;
background: #fff;
}
/* ✅ Sufficient contrast (7:1) */
.high-contrast {
color: #333;
background: #fff;
}Contrast requirements:
- Normal text: 4.5:1 (AA), 7:1 (AAA)
- Large text (18px+ or 14px+ bold): 3:1 (AA), 4.5:1 (AAA)
- UI components: 3:1
Media Alternatives (1.2)
<video controls>
<source src="video.mp4" type="video/mp4">
<track kind="captions" src="captions.vtt" srclang="en" label="English" default>
</video>Operable
Keyboard Navigation (2.1.1)
// ❌ Only click
element.addEventListener('click', handleAction);
// ✅ Click + keyboard
element.addEventListener('click', handleAction);
element.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleAction();
}
});Focus Visible (2.4.7)
/* ❌ Never remove focus */
*:focus { outline: none; }
/* ✅ Keyboard-only focus */
:focus-visible {
outline: 2px solid #005fcc;
outline-offset: 2px;
}Skip Links (2.4.1)
<body>
<a href="#main-content" class="skip-link">Skip to main content</a>
<header><!-- navigation --></header>
<main id="main-content" tabindex="-1">
<!-- content -->
</main>
</body>.skip-link {
position: absolute;
top: -40px;
left: 0;
background: #000;
color: #fff;
padding: 8px 16px;
z-index: 100;
}
.skip-link:focus {
top: 0;
}Reduced Motion (2.3.3)
@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)
<!-- ❌ No language -->
<html>
<!-- ✅ Language specified -->
<html lang="en">
<!-- ✅ Language changes -->
<p>The French word for hello is <span lang="fr">bonjour</span>.</p>Form Labels (3.3.2)
<!-- ❌ No label -->
<input type="email" placeholder="Email">
<!-- ✅ Explicit label -->
<label for="email">Email address</label>
<input type="email" id="email" autocomplete="email">
<!-- ✅ With hint -->
<label for="password">Password</label>
<input type="password" id="password" aria-describedby="password-requirements">
<p id="password-requirements">
Must be at least 8 characters with one number.
</p>Error Handling (3.3.1)
<label for="email">Email</label>
<input type="email" id="email"
aria-invalid="true"
aria-describedby="email-error">
<p id="email-error" role="alert">
Please enter a valid email address.
</p>Robust
ARIA Usage (4.1.2)
<!-- ❌ Unnecessary ARIA -->
<button role="button">Submit</button>
<!-- ✅ Native HTML -->
<button>Submit</button>
<!-- ✅ ARIA when needed (custom tabs) -->
<div role="tablist" aria-label="Product information">
<button role="tab" aria-selected="true" aria-controls="panel-1">
Description
</button>
<button role="tab" aria-selected="false" aria-controls="panel-2" tabindex="-1">
Reviews
</button>
</div>
<div role="tabpanel" id="panel-1" aria-labelledby="tab-1">
<!-- content -->
</div>Live Regions (4.1.3)
<!-- Polite (waits for pause) -->
<div aria-live="polite" aria-atomic="true">
Status update
</div>
<!-- Assertive (interrupts) -->
<div role="alert" aria-live="assertive">
Error: Form submission failed
</div>Visually Hidden Text
.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;
}<button>
<svg aria-hidden="true"><!-- icon --></svg>
<span class="visually-hidden">Delete item</span>
</button>---
Output Format
Generate reports structured as:
# Accessibility Audit Report
## Summary
- Total Issues: X
- Critical: X | Serious: X | Moderate: X | Minor: X
- WCAG Level: A, AA, or AAA
- Automated Coverage: ~57% (manual testing required)
## Critical Issues (Fix Immediately)
### 1. [Issue Name] - WCAG X.X.X
**Severity:** Critical
**Impact:** [Who is affected and how]
**Affected:** X elements
**Locations:**
- `path/to/file.tsx:123`
- `path/to/file.tsx:456`
**Problem:**
[Brief description]
**Fix:**// Before <div onClick={handleClick}>Click me</div>
// After <button onClick={handleClick}>Click me</button>
**Why:** [Accessibility principle]
---
## Serious Issues
[Same format]
## Moderate Issues
[Same format]
## Testing Recommendations
1. Manual keyboard testing (Tab, Enter, Escape)
2. Screen reader testing (see references/screen-reader-guide.md)
3. Automated testing setup (@axe-core/react or Lighthouse CI)
4. Color contrast validation (WebAIM Contrast Checker)
## Next Steps
[Prioritized action items]---
Tools & Resources
Development Tools
- eslint-plugin-jsx-a11y - React/JSX static analysis (~37 rules)
- axe-core DevTools - Browser extension for runtime testing
- Lighthouse - Built into Chrome DevTools
Testing Tools
- @axe-core/react - Runtime accessibility testing
- @axe-core/playwright - E2E test integration
- pa11y - Automated command-line testing
Manual Testing
- WebAIM Contrast Checker - https://webaim.org/resources/contrastchecker/
- WAVE - Browser extension for visual feedback
- Screen readers - NVDA (Windows), VoiceOver (macOS), JAWS
Reference Docs
references/WCAG-criteria.md- All WCAG 2.1 success criteriareferences/ARIA-patterns.md- Common ARIA patterns and examplesreferences/screen-reader-guide.md- Testing commands and scenariosreferences/grep-patterns.md- Search patterns for code audits
References
- WebAIM Million - Annual analysis of top 1M websites (violation statistics)
- WCAG 2.1 Quick Reference - Interactive WCAG guide
- WAI-ARIA Authoring Practices - Official ARIA patterns
- Deque axe Rules - All axe-core rules explained
- jsx-a11y Rules - ESLint accessibility rules
---
Important Notes
- Automated tools catch 30-57% of issues; manual testing required
- Pages with ARIA average 41% more errors than without
- Always test with actual assistive technology when possible
- Focus on critical issues first (keyboard, screen readers, contrast)
- Document deliberate accessibility decisions
- Test on multiple browsers and devices
- Include users with disabilities in testing when possible
Common Pitfalls to Avoid
1. Relying solely on automated testing 2. Using ARIA when native HTML suffices 3. Removing focus indicators 4. Using positive tabindex values 5. Color as only means of conveying information 6. Keyboard traps in modals/dialogs 7. Non-descriptive link text 8. Missing or incorrect heading hierarchy 9. Unlabeled form controls 10. Missing language attributes
ARIA Patterns and Examples
Common ARIA patterns for accessible components. Prefer native HTML elements when possible.
First Rule of ARIA
Use native HTML elements whenever possible
<!-- ❌ Don't use ARIA when native HTML works -->
<div role="button" tabindex="0">Click me</div>
<!-- ✅ Use native HTML -->
<button>Click me</button>Buttons
<!-- Native button (preferred) -->
<button>Submit</button>
<!-- Icon button with label -->
<button aria-label="Close dialog">×</button>
<!-- Button with visually hidden text -->
<button>
<svg aria-hidden="true"><!-- icon --></svg>
<span class="visually-hidden">Open menu</span>
</button>Links
<!-- Standard link -->
<a href="/page">Descriptive link text</a>
<!-- External link -->
<a href="https://external.com" target="_blank" rel="noopener">
External site
<span class="visually-hidden">(opens in new tab)</span>
</a>
<!-- Current page indicator -->
<a href="/" aria-current="page">Home</a>Form Fields
Basic input with label
<label for="email">Email address</label>
<input type="email" id="email" name="email"
autocomplete="email" required>Input with hint text
<label for="email">Email</label>
<input type="email" id="email" aria-describedby="email-hint">
<p id="email-hint">We'll never share your email.</p>Input with error
<label for="email">Email</label>
<input type="email" id="email"
aria-invalid="true"
aria-describedby="email-error">
<p id="email-error" role="alert">
Please enter a valid email address.
</p>Password with requirements
<label for="password">Password</label>
<input type="password" id="password"
aria-describedby="password-requirements">
<p id="password-requirements">
Must be at least 8 characters with one number.
</p>Navigation
Main navigation
<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>Multiple navigation regions
<!-- Each nav needs unique label -->
<nav aria-label="Main">...</nav>
<nav aria-label="Footer">...</nav>
<nav aria-label="Social media">...</nav>Modals/Dialogs
<div role="dialog"
aria-modal="true"
aria-labelledby="dialog-title"
aria-describedby="dialog-desc">
<h2 id="dialog-title">Confirm Action</h2>
<p id="dialog-desc">Are you sure you want to continue?</p>
<button>Confirm</button>
<button>Cancel</button>
</div>Focus trap implementation
function openModal(modal) {
const focusableElements = modal.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
// Trap focus within modal
modal.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
if (e.shiftKey && document.activeElement === firstElement) {
e.preventDefault();
lastElement.focus();
} else if (!e.shiftKey && document.activeElement === lastElement) {
e.preventDefault();
firstElement.focus();
}
}
if (e.key === 'Escape') {
closeModal();
}
});
firstElement.focus();
}Tabs
<div role="tablist" aria-label="Product information">
<button role="tab"
id="tab-1"
aria-selected="true"
aria-controls="panel-1">
Description
</button>
<button role="tab"
id="tab-2"
aria-selected="false"
aria-controls="panel-2"
tabindex="-1">
Reviews
</button>
</div>
<div role="tabpanel"
id="panel-1"
aria-labelledby="tab-1">
<!-- Panel content -->
</div>
<div role="tabpanel"
id="panel-2"
aria-labelledby="tab-2"
hidden>
<!-- Panel content -->
</div>Live Regions
Polite announcements (waits for pause)
<div aria-live="polite" aria-atomic="true" class="status">
<!-- Content updates announced to screen readers -->
</div>Assertive alerts (interrupts immediately)
<div role="alert" aria-live="assertive">
<!-- Urgent notifications -->
</div>Status messages
<div role="status" aria-live="polite">
Loading complete
</div>Dynamic notification example
function showNotification(message, type = 'polite') {
const container = document.getElementById(`${type}-announcer`);
container.textContent = ''; // Clear first
requestAnimationFrame(() => {
container.textContent = message;
});
}Skip Links
<body>
<a href="#main-content" class="skip-link">Skip to main content</a>
<header><!-- navigation --></header>
<main id="main-content" tabindex="-1">
<!-- main content -->
</main>
</body>.skip-link {
position: absolute;
top: -40px;
left: 0;
background: #000;
color: #fff;
padding: 8px 16px;
z-index: 100;
}
.skip-link:focus {
top: 0;
}Visually Hidden Text
For screen readers only (hidden visually but announced):
.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;
}Usage:
<button>
<svg aria-hidden="true"><!-- icon --></svg>
<span class="visually-hidden">Delete item</span>
</button>Landmark Regions
<header><!-- Banner landmark --></header>
<nav aria-label="Main"><!-- Navigation landmark --></nav>
<main><!-- Main landmark --></main>
<aside><!-- Complementary landmark --></aside>
<footer><!-- Contentinfo landmark --></footer>When to Use ARIA
✅ Good use cases:
- Custom widgets not available in HTML (tabs, accordions, complex menus)
- Live regions for dynamic content updates
- Complex application states
- Enhanced semantics for existing elements
❌ Avoid ARIA for:
- Standard form controls (use native HTML)
- Buttons and links (use
<button>and<a>) - Basic page structure (use semantic HTML5)
- Anything that can be done with native HTML
ARIA Attributes Reference
Common attributes
aria-label: Provides accessible namearia-labelledby: References element ID(s) for namearia-describedby: References element ID(s) for descriptionaria-hidden: Hides from assistive tech (use sparingly)aria-live: Announces dynamic content changesaria-current: Indicates current item in set
State attributes
aria-expanded: Collapsible element statearia-selected: Selection statearia-checked: Checkbox/radio statearia-pressed: Toggle button statearia-invalid: Form validation statearia-disabled: Disabled state
Relationship attributes
aria-controls: Element(s) this controlsaria-owns: Element(s) this owns in DOMaria-activedescendant: Active child element
Grep Patterns for Accessibility Auditing
Search patterns for finding common accessibility violations in codebases.
Missing Alt Text
Images without alt attribute
grep -rn "<img[^>]*src" \
--include="*.jsx" \
--include="*.tsx" \
--include="*.html" \
--include="*.vue" \
--include="*.astro" \
. | grep -v "alt="Images with redundant alt text
grep -rn 'alt="image\|alt="picture\|alt="photo\|alt="img' \
--include="*.jsx" \
--include="*.tsx" \
--include="*.html" \
--include="*.vue" \
--include="*.astro" \
.Keyboard Navigation
onClick without keyboard handlers
grep -rn "onClick=" \
--include="*.jsx" \
--include="*.tsx" \
. | grep -v "onKeyDown\|onKeyPress\|onKeyUp\|<button\|<a "Positive tabIndex values
grep -rn 'tabIndex="\?[1-9]' \
--include="*.jsx" \
--include="*.tsx" \
--include="*.html" \
.Focus indicators removed
grep -rn "outline:\s*none\|outline:\s*0" \
--include="*.css" \
--include="*.scss" \
--include="*.sass" \
--include="*.less" \
. | grep "focus"Color Contrast
Extract hex colors
grep -rh "#[0-9a-fA-F]\{3,8\}" \
--include="*.css" \
--include="*.scss" \
--include="*.sass" \
--include="*.less" \
--include="*.tsx" \
--include="*.jsx" \
. | grep -o "#[0-9a-fA-F]\{3,8\}" | sort -uExtract RGB/RGBA colors
grep -rh "rgba\?\([^)]*)" \
--include="*.css" \
--include="*.scss" \
--include="*.sass" \
--include="*.less" \
. | grep -o "rgba\?\([^)]*)" | sort -uARIA Issues
Find ARIA attributes
grep -rn "aria-" \
--include="*.jsx" \
--include="*.tsx" \
--include="*.html" \
.Find role attributes
grep -rn 'role=' \
--include="*.jsx" \
--include="*.tsx" \
--include="*.html" \
.Form Labels
Inputs without labels
grep -rn "<input" \
--include="*.jsx" \
--include="*.tsx" \
--include="*.html" \
. | grep -v "aria-label\|aria-labelledby"Form elements without associated labels
grep -rn "<input\|<select\|<textarea" \
--include="*.jsx" \
--include="*.tsx" \
--include="*.html" \
. | grep -v "id=\|aria-label"Heading Structure
Find all headings
grep -rn "<h[1-6]" \
--include="*.jsx" \
--include="*.tsx" \
--include="*.html" \
.Check for h1 presence
grep -r "<h1" \
--include="*.jsx" \
--include="*.tsx" \
--include="*.html" \
.Language Attributes
Check for lang attribute on html
grep -rn "<html" \
--include="*.html" \
--include="*.astro" \
. | grep -v 'lang='Link Text
Find potentially ambiguous link text
grep -rn 'click here\|here\|read more\|learn more\|link' \
--include="*.jsx" \
--include="*.tsx" \
--include="*.html" \
. | grep -i "<a"Media Elements
Videos without captions
grep -rn "<video" \
--include="*.jsx" \
--include="*.tsx" \
--include="*.html" \
. | grep -v "<track"Audio without transcripts
grep -rn "<audio" \
--include="*.jsx" \
--include="*.tsx" \
--include="*.html" \
.Usage Notes
- All patterns use
-rnfor recursive search with line numbers - Customize
--includepatterns based on your project's file types - Pipe results through
wc -lto count occurrences - Use
> output.txtto save results to file - Patterns may have false positives - manual review required
- Combine patterns with context flags:
-A 2(after),-B 2(before),-C 2(context)
Screen Reader Testing Guide
Manual testing with screen readers is essential for accessibility validation. Automated tools only catch 30-57% of issues.
Testing Checklist
Keyboard Navigation
- [ ] Tab through entire page, use Enter/Space to activate
- [ ] Can reach all interactive elements
- [ ] Focus order is logical and follows visual order
- [ ] No keyboard traps
- [ ] Skip links work properly
Screen Reader Testing
- [ ] Test with VoiceOver (Mac), NVDA (Windows), or TalkBack (Android)
- [ ] All images have meaningful alt text or are properly marked decorative
- [ ] Form labels are announced correctly
- [ ] Heading structure makes sense
- [ ] Links are descriptive
- [ ] Error messages are announced
- [ ] Dynamic content updates are announced (live regions)
Visual Testing
- [ ] Content usable at 200% zoom
- [ ] Content reflows without horizontal scroll at 320px width
- [ ] Color contrast meets WCAG requirements
- [ ] Content visible in Windows High Contrast Mode
Motion & Animation
- [ ] Test with
prefers-reduced-motion: reduce - [ ] Animations can be paused/stopped
- [ ] No content flashes more than 3 times per second
Screen Reader Commands
VoiceOver (macOS)
| Action | Command |
|---|---|
| Start/Stop | ⌘ + F5 |
| Navigate next | VO + → |
| Navigate previous | VO + ← |
| Activate element | VO + Space |
| Read from current position | VO + A |
| Stop reading | Control |
| Open rotor | VO + U |
| Navigate by headings | VO + ⌘ + H |
| Navigate by links | VO + ⌘ + L |
| Navigate by form controls | VO + ⌘ + J |
| Navigate by landmarks | VO + ⌘ + W |
Note: VO = Control + Option
NVDA (Windows)
| Action | Command |
|---|---|
| Start/Stop | Ctrl + Alt + N |
| Navigate next | ↓ |
| Navigate previous | ↑ |
| Activate element | Enter |
| Read from current position | NVDA + ↓ |
| Stop reading | Ctrl |
| Elements list | NVDA + F7 |
| Next heading | H |
| Previous heading | Shift + H |
| Next link | K |
| Previous link | Shift + K |
| Next form field | F |
| Previous form field | Shift + F |
| Next landmark | D |
| Previous landmark | Shift + D |
Note: NVDA = Insert (or Caps Lock if configured)
JAWS (Windows)
| Action | Command |
|---|---|
| Navigate next | ↓ |
| Navigate previous | ↑ |
| Next heading | H |
| Previous heading | Shift + H |
| Next link | Tab or K |
| Next form field | F |
| Next landmark | R |
| Elements list | Insert + F3 |
| Forms list | Insert + F5 |
| Links list | Insert + F7 |
| Headings list | Insert + F6 |
TalkBack (Android)
| Action | Gesture |
|---|---|
| Activate | Double-tap |
| Navigate next | Swipe right |
| Navigate previous | Swipe left |
| Scroll down | Two-finger swipe up |
| Scroll up | Two-finger swipe down |
| Global context menu | Swipe down then right |
| Local context menu | Swipe up then right |
| Reading controls | Swipe left then right |
Common Testing Scenarios
Test Form Submission
1. Navigate to form with screen reader 2. Verify each label is announced 3. Fill out form using keyboard only 4. Submit with invalid data 5. Verify error messages are announced 6. Verify focus moves to first error 7. Fix errors and submit successfully
Test Modal/Dialog
1. Open modal with keyboard 2. Verify focus moves to modal 3. Verify modal title is announced 4. Tab through modal elements 5. Verify focus stays trapped in modal 6. Press Escape to close 7. Verify focus returns to trigger element
Test Dynamic Content
1. Trigger content update 2. Verify screen reader announces change 3. Check if announcement is polite or assertive 4. Verify new content is focusable if needed
Test Navigation
1. Navigate to page with screen reader 2. Use rotor/elements list to view headings 3. Verify heading hierarchy makes sense 4. Use landmarks to navigate page sections 5. Verify skip link appears on focus 6. Test skip link functionality
Setting Up Screen Readers
VoiceOver (macOS)
- Built into macOS
- Enable: System Preferences → Accessibility → VoiceOver
- Quick toggle: ⌘ + F5
- VoiceOver Utility for settings:
/System/Library/CoreServices/VoiceOver.app - Practice mode: VoiceOver Utility → Quick Start
NVDA (Windows)
- Free, open source
- Download: https://www.nvaccess.org/
- Portable version available
- Add-ons available for enhanced testing
- Can run alongside JAWS
JAWS (Windows)
- Commercial (expensive)
- Download: https://www.freedomscientific.com/
- 40-minute demo mode available
- Most commonly used by professionals
- Excellent for professional testing
TalkBack (Android)
- Built into Android
- Enable: Settings → Accessibility → TalkBack
- Tutorial available on first run
- Test on real device preferred over emulator
Browser Extensions for Testing
axe DevTools
- Available for Chrome, Firefox, Edge
- Free browser extension
- Automated testing + guided tests
- https://www.deque.com/axe/
WAVE
- Browser extension
- Visual feedback on accessibility issues
- https://wave.webaim.org/extension/
Lighthouse
- Built into Chrome DevTools
- Accessibility audit included
- DevTools → Lighthouse tab
Best Practices
Do
- Test with actual screen reader users when possible
- Test on multiple screen readers
- Test with keyboard only before using screen reader
- Document issues with screenshots and recordings
- Test in different browsers
- Test on mobile devices
Don't
- Rely solely on automated testing
- Assume one screen reader represents all
- Test only in one browser
- Skip keyboard-only testing
- Ignore warnings from automated tools
- Test only desktop (mobile is critical)
Common Issues to Listen For
- Form fields with no label
- Images with no alt text or poor alt text
- Links with non-descriptive text ("click here")
- Headings out of order or missing
- Tables with no headers
- Live regions not announcing updates
- Modal dialogs not announced
- Focus moving unexpectedly
- Content only available visually (color, position)
- Redundant or verbose announcements
Resources
WCAG 2.1 Quick Reference
Success criteria by level
Level A (minimum)
| Criterion | Description |
|---|---|
| 1.1.1 Non-text Content | All images, icons have text alternatives |
| 1.2.1 Audio-only/Video-only | Provide transcript or audio description |
| 1.2.2 Captions | Video with audio has captions |
| 1.2.3 Audio Description | Video has audio description |
| 1.3.1 Info and Relationships | Information conveyed through presentation is available programmatically |
| 1.3.2 Meaningful Sequence | Reading order is logical |
| 1.3.3 Sensory Characteristics | Instructions don't rely solely on shape, color, size, location, orientation, or sound |
| 1.4.1 Use of Color | Color is not the only visual means of conveying information |
| 1.4.2 Audio Control | Audio playing automatically can be paused/stopped |
| 2.1.1 Keyboard | All functionality available via keyboard |
| 2.1.2 No Keyboard Trap | Keyboard focus can be moved away from any component |
| 2.1.4 Character Key Shortcuts | Single-key shortcuts can be turned off or remapped |
| 2.2.1 Timing Adjustable | Time limits can be extended |
| 2.2.2 Pause, Stop, Hide | Moving/blinking content can be paused |
| 2.3.1 Three Flashes | Nothing flashes more than 3 times per second |
| 2.4.1 Bypass Blocks | Skip link or landmark navigation available |
| 2.4.2 Page Titled | Pages have descriptive titles |
| 2.4.3 Focus Order | Focus order preserves meaning |
| 2.4.4 Link Purpose | Link purpose clear from link text or context |
| 2.5.1 Pointer Gestures | Multi-point gestures have single-pointer alternatives |
| 2.5.2 Pointer Cancellation | Down-event doesn't trigger action (use up-event or click) |
| 2.5.3 Label in Name | Accessible name contains visible label text |
| 2.5.4 Motion Actuation | Motion-triggered functions have alternatives |
| 3.1.1 Language of Page | Default language specified in HTML |
| 3.2.1 On Focus | Focus doesn't trigger unexpected changes |
| 3.2.2 On Input | Input doesn't trigger unexpected changes |
| 3.3.1 Error Identification | Input errors clearly described |
| 3.3.2 Labels or Instructions | Form inputs have labels or instructions |
| 4.1.1 Parsing | HTML is well-formed (no duplicate IDs, proper nesting) |
| 4.1.2 Name, Role, Value | UI components have accessible names and correct roles |
Level AA (standard)
| Criterion | Description |
|---|---|
| 1.2.4 Captions (Live) | Live audio has captions |
| 1.2.5 Audio Description | Pre-recorded video has audio description |
| 1.3.4 Orientation | Content doesn't restrict orientation |
| 1.3.5 Identify Input Purpose | Input purpose can be programmatically determined |
| 1.4.3 Contrast (Minimum) | 4.5:1 for normal text, 3:1 for large text |
| 1.4.4 Resize Text | Text can be resized to 200% without loss of functionality |
| 1.4.5 Images of Text | Text used instead of images of text |
| 1.4.10 Reflow | Content reflows at 320px width without horizontal scroll |
| 1.4.11 Non-text Contrast | UI components have 3:1 contrast |
| 1.4.12 Text Spacing | Content adapts to text spacing changes |
| 1.4.13 Content on Hover/Focus | Additional content is dismissible, hoverable, persistent |
| 2.4.5 Multiple Ways | Multiple ways to find pages |
| 2.4.6 Headings and Labels | Headings and labels are descriptive |
| 2.4.7 Focus Visible | Focus indicator is visible |
| 3.1.2 Language of Parts | Language changes are marked |
| 3.2.3 Consistent Navigation | Navigation is consistent across pages |
| 3.2.4 Consistent Identification | Same functionality uses same labels |
| 3.3.3 Error Suggestion | Error corrections suggested when known |
| 3.3.4 Error Prevention (Legal) | Actions can be reversed or confirmed |
| 4.1.3 Status Messages | Status messages announced to screen readers |
Level AAA (enhanced)
| Criterion | Description |
|---|---|
| 1.4.6 Contrast (Enhanced) | 7:1 for normal text, 4.5:1 for large text |
| 1.4.8 Visual Presentation | Foreground/background colors can be selected |
| 1.4.9 Images of Text (No Exception) | No images of text |
| 2.1.3 Keyboard (No Exception) | All functionality keyboard accessible |
| 2.2.3 No Timing | No time limits |
| 2.2.4 Interruptions | Interruptions can be postponed |
| 2.2.5 Re-authenticating | Data preserved on re-authentication |
| 2.2.6 Timeouts | Users warned about data loss from inactivity |
| 2.3.2 Three Flashes | No content flashes more than 3 times |
| 2.3.3 Animation from Interactions | Motion animation can be disabled |
| 2.4.8 Location | User location within site is available |
| 2.4.9 Link Purpose (Link Only) | Link purpose clear from link text alone |
| 2.4.10 Section Headings | Sections have headings |
| 3.1.3 Unusual Words | Definitions available for unusual words |
| 3.1.4 Abbreviations | Abbreviations expanded |
| 3.1.5 Reading Level | Alternative content for complex text |
| 3.1.6 Pronunciation | Pronunciation available where needed |
| 3.2.5 Change on Request | Changes initiated only by user |
| 3.3.5 Help | Context-sensitive help available |
| 3.3.6 Error Prevention (All) | All form submissions can be reviewed |
Common ARIA patterns
Buttons
<button>Label</button>
<!-- or -->
<button aria-label="Close dialog">×</button>Links
<a href="/page">Descriptive link text</a>
<!-- External links -->
<a href="https://external.com" target="_blank" rel="noopener">
External site
<span class="visually-hidden">(opens in new tab)</span>
</a>Form fields
<label for="email">Email address</label>
<input type="email" id="email" aria-describedby="email-hint">
<p id="email-hint">We'll never share your email.</p>Error states
<label for="email">Email</label>
<input type="email" id="email" aria-invalid="true" aria-describedby="email-error">
<p id="email-error" role="alert">Please enter a valid email address.</p>Navigation
<nav aria-label="Main">
<ul>
<li><a href="/" aria-current="page">Home</a></li>
<li><a href="/about">About</a></li>
</ul>
</nav>Modals
<div role="dialog" aria-modal="true" aria-labelledby="dialog-title">
<h2 id="dialog-title">Confirm Action</h2>
<!-- content -->
</div>Live regions
<!-- Polite (waits for pause in speech) -->
<div aria-live="polite">Status update here</div>
<!-- Assertive (interrupts immediately) -->
<div aria-live="assertive" role="alert">Error message here</div>
<!-- Status (polite, implicit) -->
<div role="status">Loading complete</div>Testing tools
| Tool | Type | URL |
|---|---|---|
| axe DevTools | Browser extension | deque.com/axe |
| WAVE | Browser extension | wave.webaim.org |
| Lighthouse | Built into Chrome | DevTools → Lighthouse |
| NVDA | Screen reader (Windows) | nvaccess.org |
| VoiceOver | Screen reader (Mac) | Built into macOS |
| Colour Contrast Analyser | Desktop app | tpgi.com |
#!/bin/bash
# Run ESLint with jsx-a11y rules for accessibility checking
set -e
OUTPUT_DIR=".claude/skills/a11y-auditor"
mkdir -p "$OUTPUT_DIR"
echo "Running ESLint accessibility checks..."
# Check if eslint is available
if ! command -v npx &> /dev/null; then
echo "Error: npx not found. Please install Node.js and npm."
exit 1
fi
# Run ESLint and capture output
npx eslint \
--ext .jsx,.tsx \
--no-ignore \
--format json \
. > "$OUTPUT_DIR/eslint-results.json" 2>&1 || true
# Also generate a readable format
npx eslint \
--ext .jsx,.tsx \
--no-ignore \
. > "$OUTPUT_DIR/eslint-results.txt" 2>&1 || true
echo "Results saved to:"
echo " - $OUTPUT_DIR/eslint-results.json (machine-readable)"
echo " - $OUTPUT_DIR/eslint-results.txt (human-readable)"
# Count violations
if [ -f "$OUTPUT_DIR/eslint-results.json" ]; then
VIOLATIONS=$(grep -o '"ruleId":"jsx-a11y' "$OUTPUT_DIR/eslint-results.json" | wc -l || echo "0")
echo ""
echo "Found $VIOLATIONS jsx-a11y violations"
fi