This project includes comprehensive accessibility testing to ensure inclusive user experiences for all users, including those using assistive technologies.
We use multiple tools and approaches to ensure accessibility:
- axe-core - Automated accessibility testing engine
- jest-axe - Integration of axe-core with our Vitest test suite
- Storybook a11y addon - Visual accessibility checks during component development
- eslint-plugin-jsx-a11y - Static code analysis for accessibility issues
pnpm testpnpm test:watchpnpm test -- --reporter=verboseUse the provided utility functions in src/lib/test-utils/accessibility.ts:
import { render } from '@testing-library/react';
import { expectNoA11yViolations } from '@/lib/test-utils/accessibility';
import { MyComponent } from './my-component';
describe('MyComponent Accessibility', () => {
it('should have no accessibility violations', async () => {
const renderResult = render(<MyComponent />);
await expectNoA11yViolations(renderResult);
});
});import { render } from '@testing-library/react';
import { checkA11yWithOptions } from '@/lib/test-utils/accessibility';
it('should pass specific accessibility rules', async () => {
const { container } = render(<MyComponent />);
const results = await checkA11yWithOptions(container, {
rules: {
'color-contrast': { enabled: true },
'aria-required-attr': { enabled: true }
}
});
expect(results).toHaveNoViolations();
});The Storybook a11y addon is automatically enabled for all stories. It provides:
- Real-time accessibility violation detection
- WCAG compliance level indicators
- Detailed violation information
- Visual highlights of problematic elements
- Start Storybook:
pnpm storybook - Navigate to any story
- Click the "Accessibility" tab in the addons panel
- Review any violations and their severity
The following accessibility rules are enforced during linting:
jsx-a11y/alt-text- Requires alt text on imagesjsx-a11y/aria-props- Validates ARIA propertiesjsx-a11y/aria-proptypes- Validates ARIA property typesjsx-a11y/aria-unsupported-elements- Prevents ARIA on unsupported elementsjsx-a11y/role-has-required-aria-props- Ensures required ARIA props are presentjsx-a11y/role-supports-aria-props- Validates ARIA props for rolesjsx-a11y/heading-has-content- Ensures headings have contentjsx-a11y/html-has-lang- Requires lang attribute on html elementjsx-a11y/iframe-has-title- Requires title on iframesjsx-a11y/img-redundant-alt- Prevents redundant alt textjsx-a11y/no-aria-hidden-on-focusable- Prevents aria-hidden on focusable elementsjsx-a11y/no-autofocus- Warns against autofocus (can be disruptive)jsx-a11y/no-redundant-roles- Prevents redundant role attributes
pnpm lintAccessibility tests are automatically run in the CI pipeline:
- Test Job - Runs all unit tests including accessibility tests
- Accessibility Job - Dedicated job that explicitly runs accessibility tests
- Lint Job - Includes ESLint accessibility rule checks
The CI pipeline will fail if:
- Any accessibility test fails
- Critical ESLint accessibility rules are violated
- WCAG violations are detected by axe-core
Every interactive component (buttons, forms, modals, etc.) should have accessibility tests.
Test components in all their states (open/closed, enabled/disabled, error/success).
describe('Button Accessibility', () => {
it('should be accessible when enabled', async () => {
const renderResult = render(<Button>Click me</Button>);
await expectNoA11yViolations(renderResult);
});
it('should be accessible when disabled', async () => {
const renderResult = render(<Button disabled>Click me</Button>);
await expectNoA11yViolations(renderResult);
});
});- Use
<button>for buttons, not<div onClick={...}> - Use
<a>for links - Use proper heading hierarchy (
<h1>,<h2>, etc.) - Use
<label>for form inputs
<button aria-label="Close dialog">
<CloseIcon />
</button>- Text should have at least 4.5:1 contrast ratio with background
- Large text can have 3:1 contrast ratio
- All interactive elements should be focusable
- Custom controls should support keyboard navigation
- Use
tabIndexappropriately
// ❌ Bad
<img src="/logo.png" />
// ✅ Good
<img src="/logo.png" alt="Company Logo" />
// ✅ Good for decorative images
<img src="/decorative.png" alt="" role="presentation" />// ❌ Bad
<input type="text" />
// ✅ Good
<label htmlFor="name">Name:</label>
<input id="name" type="text" />
// ✅ Good (using aria-label)
<input type="text" aria-label="Search" />// ❌ Bad
<button><Icon /></button>
// ✅ Good
<button aria-label="Delete item">
<TrashIcon />
</button>// ❌ Bad
<h1>Page Title</h1>
<h3>Section</h3> // Skipped h2
// ✅ Good
<h1>Page Title</h1>
<h2>Section</h2>
<h3>Subsection</h3>If you have questions about accessibility or need help fixing violations:
- Check the violation message - it usually includes helpful suggestions
- Review the resources above
- Use the Storybook a11y addon to see violations visually
- Ask the team in your PR review
Remember: Accessibility is not optional - it's a fundamental requirement for inclusive web experiences.