Testing Accessibility
Automated testing, manual testing, axe-core, Lighthouse, and accessibility testing strategies.
Question: How do you test accessibility in frontend applications?β
Difficulty: π‘ Medium | Frequency: ββββ | Companies: Google, Airbnb
Testing Strategyβ
Automated tools catch approximately 30% of accessibility issues. The rest require manual testing and real user validation.
| Layer | Tools | Catches |
|---|---|---|
| Unit/Component | jest-axe, @testing-library/react | Missing labels, role issues |
| Integration | cypress-axe | Page-level contrast, structure |
| Browser | axe DevTools, WAVE | All automated checks |
| Manual | Keyboard + screen reader | Focus management, announcements |
Automated Testingβ
jest-axe (Component Tests)β
import { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);
test('should have no accessibility violations', async () => {
const { container } = render(<Button>Click me</Button>);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
test('form is accessible', async () => {
const { container } = render(<LoginForm />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
@testing-library Accessible Queriesβ
Testing Library encourages accessible queries that mirror how screen readers discover elements:
import { render, screen } from '@testing-library/react';
test('login form is accessible', () => {
render(<LoginForm />);
// Find by label (screen-reader friendly)
const emailInput = screen.getByLabelText(/email/i);
const passwordInput = screen.getByLabelText(/password/i);
const submitButton = screen.getByRole('button', { name: /sign in/i });
expect(emailInput).toBeInTheDocument();
expect(passwordInput).toBeInTheDocument();
expect(submitButton).toBeInTheDocument();
});
// Query priority (most to least accessible):
// getByRole β getByLabelText β getByPlaceholderText β getByText β getByTestId
cypress-axe (E2E Tests)β
// cypress/support/commands.js
import 'cypress-axe';
describe('Home page', () => {
it('has no accessibility violations', () => {
cy.visit('/');
cy.injectAxe();
cy.checkA11y(); // Checks entire page
});
it('modal has no violations', () => {
cy.visit('/');
cy.injectAxe();
cy.get('[data-cy="open-modal"]').click();
cy.checkA11y('[role="dialog"]'); // Check only the modal
});
});
Storybook a11y Addonβ
// .storybook/main.js
module.exports = {
addons: ['@storybook/addon-a11y']
};
// Automatically checks all stories β see violations in Accessibility tab
Browser-Based Toolsβ
axe DevTools (Browser Extension)β
- Install from Chrome Web Store
- Open DevTools β axe DevTools tab
- Click "Scan ALL of my page"
- Review violations with issue descriptions and fix suggestions
Catches: missing labels, insufficient contrast, missing landmarks, ARIA misuse, keyboard traps.
WAVE (Web Accessibility Evaluation Tool)β
https://wave.webaim.org/
Or install the browser extension. Provides visual overlay showing:
- Structural elements (headings, landmarks)
- ARIA attributes
- Color contrast
- Errors and alerts
Lighthouse (Chrome DevTools)β
- Open DevTools β Lighthouse tab
- Check "Accessibility" category
- Click "Generate report"
- Scores 0β100, shows specific failures with WCAG references
Manual Testing Checklistβ
Keyboard Navigationβ
β‘ Tab moves forward through all interactive elements
β‘ Shift+Tab moves backward
β‘ Enter activates links and buttons
β‘ Space activates buttons
β‘ Escape closes modals and dropdowns
β‘ Arrow keys work within widgets (tabs, menus, grids)
β‘ Focus indicator is visible at all times
β‘ No keyboard traps (can always Tab away)
β‘ Skip link appears on first Tab press
Screen Reader Testingβ
β‘ Page title announced on load
β‘ Images have appropriate alt text
β‘ Form labels announced with inputs
β‘ Error messages announced immediately
β‘ Modal announces title when opened
β‘ Dynamic updates announced via live regions
β‘ Navigation landmarks work (D key in NVDA)
β‘ Headings form logical hierarchy (H key in NVDA)
β‘ Custom widgets announce state changes
Visual Checksβ
β‘ Color contrast meets AA (4.5:1 normal, 3:1 large text)
β‘ Links underlined or distinguishable without color
β‘ Focus indicator visible on all elements
β‘ Error states use more than just color
β‘ Text scales to 200% without loss of content
β‘ No content lost on small screens (320px width)
How axe-core Worksβ
axe-core analyzes the DOM + accessibility tree and applies WCAG rules:
// Simplified concept of what axe-core does
class AccessibilityAuditor {
audit(element) {
const tree = buildAccessibilityTree(element);
const violations = [];
// Rule: images must have alt text
tree.images.forEach(img => {
if (!img.alt && !img.ariaLabel && !img.ariaLabelledBy) {
violations.push({
id: 'image-alt',
impact: 'critical',
help: 'Images must have alternate text',
nodes: [img]
});
}
});
// Rule: color contrast
tree.textElements.forEach(el => {
const ratio = calculateContrastRatio(el.color, el.backgroundColor);
if (ratio < 4.5 && el.fontSize < 24) {
violations.push({
id: 'color-contrast',
impact: 'serious',
help: 'Elements must have sufficient color contrast',
nodes: [el]
});
}
});
return violations;
}
}
CI/CD Integrationβ
# GitHub Actions: run accessibility checks on every PR
name: Accessibility
on: [pull_request]
jobs:
a11y:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: npm install
- run: npm test -- --testPathPattern="a11y|accessibility"
- name: Lighthouse CI
uses: treosh/lighthouse-ci-action@v9
with:
urls: http://localhost:3000
budgetPath: ./lighthouse-budget.json
Automated vs. Manual Coverageβ
| Issue Type | Automated | Manual Required |
|---|---|---|
| Missing alt text | β | |
| Missing form labels | β | |
| Color contrast | β | |
| Missing landmarks | β | |
| Keyboard trap | β | β |
| Focus management | β | β |
| Screen reader announcements | β | β |
| Confusing UX for AT users | β | β |
Always combine automated and manual testing β automated tools are a starting point, not a guarantee.
Content from Frontend-Master-Prep-Series β 08-accessibility