Testing Components for Accessibility — Automated and Manual Testing
In this tutorial, you will learn about Testing Components for Accessibility. We cover key concepts, practical examples, and best practices to help you master this topic.
Testing components for accessibility in design systems combines automated unit tests with axe-core, manual keyboard testing, screen reader verification, contrast validation, and visual regression testing for accessibility regressions.
What You'll Learn
You will learn how to set up accessibility testing in a design system, what types of tests to run, how to integrate testing into CI/CD, and how to document testing results.
Why It Matters
Testing at the design system level catches accessibility issues once instead of in every product that uses the component. A component that passes a11y tests can be trusted across all products.
Real-World Use
DodaKit runs automated accessibility tests in CI for every pull request. Tests include axe-core scanning, keyboard navigation checks, and contrast validation. Manual testing with NVDA is done before each release.
flowchart TD A[Testing] --> B[Automated] A --> C[Manual] A --> D[Screen Reader] A --> E[Regression] B --> B1[axe-core in CI] B --> B2[Contrast check] C --> C1[Keyboard only] C --> C2[Zoom to 400%] D --> D1[NVDA, VoiceOver] E --> E1[Percy, Chromatic]
Automated Testing
Use axe-core to automate accessibility checks. Run tests in unit tests, Storybook, or integration tests. Fail the build on critical and serious violations.
Keyboard Testing
Create automated keyboard tests that tab through all interactive elements and verify focus indicators are visible at each stop.
Contrast Validation
Check color contrast programmatically for all color tokens and component color pairs. Fail if any pair violates WCAG AA.
// Component accessibility test suite
class ComponentA11yTest {
constructor(componentName) {
this.componentName = componentName;
this.results = [];
}
async runAxeCheck(html) {
// Simulated axe-core check
const violations = [];
// Check for required ARIA attributes
if (html.includes('role=')) {
const required = ['aria-label', 'aria-labelledby', 'aria-describedby'];
required.forEach(attr => {
if (html.includes('role=') && !html.includes(attr)) {
violations.push({
id: 'aria-required-attr',
impact: 'critical',
description: `ARIA role requires ${attr}`
});
}
});
}
// Check for focus-visible
if (!html.includes(':focus-visible') && !html.includes('outline')) {
violations.push({
id: 'focus-visible',
impact: 'serious',
description: 'No visible focus indicator found'
});
}
// Check for label-input association
if (html.includes('type=') && !html.includes('aria-label') && !html.includes('<label')) {
violations.push({
id: 'label',
impact: 'critical',
description: 'Input missing accessible label'
});
}
this.results.push({
test: 'axe-check',
violations: violations,
passed: violations.length === 0
});
return { violations, passed: violations.length === 0 };
}
runKeyboardCheck(interactiveCount) {
// Simulate tabbing through interactive elements
const issues = [];
const tabStops = [];
for (let i = 0; i < interactiveCount; i++) {
tabStops.push({ index: i, hasFocus: true });
}
if (tabStops.length === 0 && interactiveCount > 0) {
issues.push('No tab stops found on interactive component');
}
this.results.push({
test: 'keyboard',
tabStops: tabStops.length,
issues: issues,
passed: issues.length === 0
});
return { tabStops: tabStops.length, issues, passed: issues.length === 0 };
}
getSummary() {
const allPassed = this.results.every(r => r.passed);
return {
component: this.componentName,
totalTests: this.results.length,
passed: this.results.filter(r => r.passed).length,
failed: this.results.filter(r => !r.passed).length,
allPassed: allPassed,
details: this.results.map(r => ({
test: r.test,
passed: r.passed,
issues: r.test === 'axe-check' ? r.violations.length : r.issues.length
}))
};
}
}
const test = new ComponentA11yTest('Dropdown');
const axeResult = await test.runAxeCheck('<div class="dropdown" role="listbox"><div role="option">Item</div></div>');
console.log('Axe:', axeResult);
const kbResult = test.runKeyboardCheck(3);
console.log('Keyboard:', kbResult);
console.log('Summary:', test.getSummary());
Expected output:
Axe: { violations: [ { id: 'aria-required-attr', impact: 'critical', description: 'ARIA role requires aria-label' } ], passed: false }
Keyboard: { tabStops: 3, issues: [], passed: true }
Summary: { component: 'Dropdown', totalTests: 2, passed: 1, failed: 1, allPassed: false, details: [ { test: 'axe-check', passed: false, issues: 1 }, { test: 'keyboard', passed: true, issues: 0 } ] }
Screen Reader Testing
Test each component with NVDA on Windows and VoiceOver on macOS. Verify that content and interactions are announced correctly.
Visual Regression
Use visual regression tools like Percy or Chromatic to catch visual changes that may affect accessibility, such as contrast changes or focus indicator removal.
<!-- Testing documentation for components -->
<section aria-label="Testing results for Button component">
<h2>Button — Testing Results</h2>
<h3>Automated (axe-core 4.8)</h3>
<ul>
<li>Critical violations: 0</li>
<li>Serious violations: 0</li>
<li>Moderate violations: 0</li>
<li>Passed: Yes</li>
</ul>
<h3>Keyboard</h3>
<ul>
<li>Tab: Focuses button</li>
<li>Enter/Space: Activates button</li>
<li>Focus indicator visible: Yes</li>
<li>Passed: Yes</li>
</ul>
<h3>Screen Reader (NVDA 2024 + Chrome 125)</h3>
<ul>
<li>Button announced correctly: Yes</li>
<li>Disabled state announced: Yes</li>
<li>Toggle state announced: Yes</li>
<li>Passed: Yes</li>
</ul>
<h3>Contrast</h3>
<ul>
<li>Primary text on primary bg: 4.8:1 (Pass AA)</li>
<li>Secondary text on surface: 5.7:1 (Pass AA)</li>
<li>Disabled text on surface: 2.8:1 (Fail AA) — Known issue</li>
</ul>
</section>
Common Mistakes
1. No Automated Accessibility Tests
Without automated tests, accessibility issues are only found during manual testing or in production.
2. Ignoring axe-Core Violations
Failing tests should block PRs. Allowing critical violations into the design system propagates inaccessibility.
3. No Keyboard Testing Automation
Automated keyboard tests catch focus order and visibility issues that axe-core does not.
4. Testing Only in One Browser
Accessibility varies by browser and assistive technology. Test in Chrome, Firefox, and Safari.
5. No Screen Reader Testing
Automated tests cannot verify screen reader output. Manual testing with NVDA and VoiceOver is essential.
6. No Visual Regression for a11y
A visual change that reduces contrast or removes a focus indicator may go unnoticed without visual regression tests.
7. No Accessibility Gate in CI
Accessibility tests must be part of CI. If they are optional, they will be skipped.
Practice Questions
1. What are the three types of accessibility testing for design system components?
Automated (axe-core), manual keyboard, and screen reader testing.
2. Why should axe-core tests run in CI?
To catch accessibility issues before components are released. Failing tests should block PRs.
3. What does visual regression testing catch for accessibility?
Visual changes that may reduce contrast, resize touch targets, or remove focus indicators.
4. Which screen readers should components be tested with?
NVDA on Windows and VoiceOver on macOS. JAWS for enterprise requirements.
5. Challenge: Set up an accessibility test plan for a design system with 5 components. Include automated, keyboard, screen reader, and contrast tests.
FAQ
Mini Project
Create an accessibility test suite for 3 design system components. Include automated axe-core checks, keyboard tab order tests, contrast validation, and documentation of screen reader behavior.
What's Next
Learn about Contribution Guidelines for keeping accessibility standards in design system contributions. Then complete the Design System Project.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro