Automated tools catch approximately 30-50% of accessibility issues. They excel at finding programmatic violations like missing alt text, insufficient contrast, and improper ARIA usage. Manual testing covers the rest, but automation provides a fast, consistent baseline.
What Automation Can and Cannot Catch
| Can Detect | Cannot Detect |
|---|---|
| Missing alt text | Whether alt text is meaningful |
| Insufficient color contrast | Whether content is understandable |
| Missing form labels | Whether labels are descriptive |
| Duplicate IDs | Logical reading order |
| Missing document language | Keyboard usability |
| Invalid ARIA attributes | Whether ARIA is used correctly |
| Missing skip navigation | Whether focus management works |
axe-core: The Industry Standard
axe-core by Deque is the most widely used accessibility testing engine. It powers most other a11y tools.
Browser Extension
Install the axe DevTools extension:
1. Chrome Web Store > search "axe DevTools"
2. Install the extension
3. Open DevTools (F12) > axe DevTools tab
4. Click "Scan All of My Page"
The scan results show:
- Violations (must fix)
- Needs Review (manual check required)
- Passes (confirmed accessible)
- Incomplete (could not determine)axe-core in Unit Tests
# Install axe-core for React testing
pnpm add -D @axe-core/react jest-axe// __tests__/components/course-card.test.tsx
import { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';
import { CourseCard } from '@/app/components/course-cards';
expect.extend(toHaveNoViolations);
describe('CourseCard accessibility', () => {
it('should have no accessibility violations', async () => {
const { container } = render(
<CourseCard
title="Accessibility Testing"
summary="Learn to test for accessibility"
level="intermediate"
slug="accessibility-testing"
tags="a11y, testing"
lessonCount={5}
/>
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it('should have proper heading hierarchy', async () => {
const { container } = render(
<main>
<h1>Courses</h1>
<CourseCard
title="Accessibility Testing"
summary="Learn to test for accessibility"
level="intermediate"
slug="accessibility-testing"
tags="a11y, testing"
lessonCount={5}
/>
</main>
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
});axe-core in Playwright
// tests/a11y/course-page.spec.ts
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test.describe('Course page accessibility', () => {
test('should pass axe accessibility checks', async ({ page }) => {
await page.goto('/courses/accessibility-testing');
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag22aa'])
.analyze();
expect(results.violations).toEqual([]);
});
test('should pass axe checks on mobile viewport', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 812 });
await page.goto('/courses/accessibility-testing');
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa'])
.analyze();
expect(results.violations).toEqual([]);
});
test('should pass axe checks after interaction', async ({ page }) => {
await page.goto('/courses');
// Open a filter dropdown
await page.click('[data-testid="level-filter"]');
// Scan after the DOM has changed
const results = await new AxeBuilder({ page })
.include('[data-testid="course-list"]')
.analyze();
expect(results.violations).toEqual([]);
});
});Handling Violations in Tests
// tests/helpers/a11y-reporter.ts
import { AxeResults, Result } from 'axe-core';
export function formatViolations(violations: Result[]): string {
if (violations.length === 0) return 'No violations found';
return violations
.map((violation) => {
const nodes = violation.nodes
.map((node) => {
return ` - ${node.html}\n Fix: ${node.failureSummary}`;
})
.join('\n');
return [
`Rule: ${violation.id}`,
`Impact: ${violation.impact}`,
`Description: ${violation.description}`,
`Help: ${violation.helpUrl}`,
`Elements:\n${nodes}`,
].join('\n');
})
.join('\n\n---\n\n');
}
// Usage in tests
test('accessible homepage', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page }).analyze();
if (results.violations.length > 0) {
console.log(formatViolations(results.violations));
}
expect(results.violations).toEqual([]);
});Lighthouse Accessibility Audit
Lighthouse includes an accessibility audit powered by axe-core.
# Run Lighthouse from the command line
npx lighthouse https://localhost:3000 \
--only-categories=accessibility \
--output=html \
--output-path=./lighthouse-a11y.html
# Run in CI with Chrome headless
npx lighthouse https://staging.example.com \
--only-categories=accessibility \
--chrome-flags="--headless --no-sandbox" \
--output=json \
--output-path=./lighthouse-results.jsonLighthouse in CI/CD
# .github/workflows/a11y.yml
name: Accessibility Audit
on:
pull_request:
branches: [main]
jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
- name: Install and build
run: |
pnpm install --frozen-lockfile
pnpm build
- name: Start server
run: pnpm start &
- name: Wait for server
run: npx wait-on http://localhost:3000
- name: Run Lighthouse
uses: treosh/lighthouse-ci-action@v12
with:
urls: |
http://localhost:3000/
http://localhost:3000/courses
http://localhost:3000/blog
budgetPath: ./lighthouse-budget.json
- name: Check accessibility score
run: |
SCORE=$(jq '.categories.accessibility.score' lighthouse-results.json)
if (( $(echo "$SCORE < 0.9" | bc -l) )); then
echo "Accessibility score $SCORE is below 90%"
exit 1
fiPa11y: CLI Accessibility Testing
Pa11y provides a simple command-line interface for accessibility testing.
# Install pa11y
pnpm add -D pa11y pa11y-ci
# Scan a single page
npx pa11y https://localhost:3000/courses
# Scan with specific standard
npx pa11y --standard WCAG2AA https://localhost:3000
# Scan multiple pages with pa11y-ci
npx pa11y-ciPa11y Configuration
// .pa11yci.json
{
"defaults": {
"standard": "WCAG2AA",
"timeout": 10000,
"wait": 1000,
"chromeLaunchConfig": {
"args": ["--no-sandbox"]
}
},
"urls": [
"http://localhost:3000/",
"http://localhost:3000/courses",
"http://localhost:3000/blog",
{
"url": "http://localhost:3000/courses/accessibility-testing",
"actions": [
"wait for element #lesson-list to be visible"
]
}
]
}Color Contrast Testing
// Programmatic contrast ratio calculation
function getLuminance(r, g, b) {
const [rs, gs, bs] = [r, g, b].map((c) => {
c = c / 255;
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
});
return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
}
function getContrastRatio(rgb1, rgb2) {
const l1 = getLuminance(...rgb1);
const l2 = getLuminance(...rgb2);
const lighter = Math.max(l1, l2);
const darker = Math.min(l1, l2);
return (lighter + 0.05) / (darker + 0.05);
}
// Test brand colors
const brandRed = [226, 27, 27]; // #E21B1B
const white = [255, 255, 255];
const black = [0, 0, 0];
console.log(`Red on white: ${getContrastRatio(brandRed, white).toFixed(2)}:1`);
// Red on white: 4.63:1 -- passes AA for large text only
console.log(`Red on black: ${getContrastRatio(brandRed, black).toFixed(2)}:1`);
// Red on black: 4.53:1 -- passes AA for large text only
Building an Accessibility Test Suite
// tests/a11y/full-suite.spec.ts
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
const pages = [
{ url: '/', name: 'Homepage' },
{ url: '/courses', name: 'Courses listing' },
{ url: '/blog', name: 'Blog listing' },
{ url: '/courses/accessibility-testing', name: 'Course detail' },
{ url: '/blog/first-post', name: 'Blog post' },
];
for (const { url, name } of pages) {
test(`${name} (${url}) passes accessibility checks`, async ({ page }) => {
await page.goto(url);
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa'])
.analyze();
expect(
results.violations,
`Found ${results.violations.length} violations on ${name}`
).toEqual([]);
});
}Key Takeaways
- Automated tools catch 30-50% of accessibility issues -- use them as a baseline
- axe-core is the industry standard engine, available in browser extensions, unit tests, and E2E tests
- Integrate accessibility checks into CI/CD so regressions are caught early
- Color contrast testing can be fully automated
- Always combine automated testing with manual screen reader testing
In the final lesson, you will learn how to integrate accessibility testing into your CI/CD pipeline for continuous compliance.