Build accessible applications by integrating a11y testing into your development workflow. Covers WCAG compliance, automated scanning, manual testing, screen reader testing, and CI/CD integration.
Accessibility Testing ensures that user interfaces are usable by people with a wide range of abilities, including those who rely on screen readers, keyboard navigation, or assistive technologies. It matters most during the final validation of a UI, before release, and when integrating new components into a shared design system. It is the definitive check that a web application can be traversed and understood by someone with limited vision, motor control, or cognition.
Accessibility testing begins with defining the scope of what must be tested, based on user needs and platform constraints. A test plan should include:
The test plan must specify the test environment: browser, screen reader, and device. Common configurations include:
Each environment must be tested with a known set of default settings, including screen reader verbosity, keyboard focus styles, and zoom levels (150% and 200%).
Manual testing is foundational and reveals subtle issues that automated tools miss. The tester moves through the application using only a keyboard and a screen reader, verifying that:
To verify keyboard navigation, use the following pattern:
The test is considered complete when the following conditions are met:
tabindex attribute is used appropriately, with tabindex="0" for default tab order and tabindex="-1" for dynamic focus.Use a screen reader in “read mode” to verify that:
A common failure is that aria-live="polite" regions are not announced when content changes, or that aria-label values are missing on icons.
Axe is a widely adopted, open-source accessibility testing engine. It can be used in browser, Node.js, and CI environments. The core workflow uses the axe-core library with a consistent configuration.
Axe accepts a JSON configuration object with the following keys:
{
"runOnly": {
"type": "tag",
"values": ["wcag2a", "wcag2aa", "best-practice"]
},
"includedRules": [
"color-contrast",
"landmark-roles",
"aria-allowed-attr",
"aria-allowed-aria-attr",
"aria-required-attr",
"aria-required-children",
"aria-roles",
"aria-unsupported-aria-attr"
],
"exclude": [
".skip-accessibility-test",
"[aria-hidden=true]"
],
"reporter": "v2"
}
This configuration ensures that the test runs only the WCAG 2.1 A and AA checks, includes specific rules, skips elements with aria-hidden=true, and excludes elements with the class skip-accessibility-test.
In a CI pipeline, use axe-core via jest or playwright to run tests against a built application.
With Playwright, test a page with:
const { test, expect } = require('@playwright/test');
const { axe, configure } = require('axe-core');
test('homepage accessibility', async ({ page }) => {
await page.goto('https://example.com');
await configure({
runOnly: {
type: 'tag',
values: ['wcag2aa', 'best-practice']
},
rules: {
'color-contrast': { enabled: true },
'landmark-roles': { enabled: true }
},
exclude: ['.skip-accessibility-test']
});
const results = await axe(page.evaluate(() => document));
const violations = results.violations;
expect(violations).toHaveLength(0);
});
This script runs Axe on the homepage, configures it to focus on WCAG 2.1 AA and best practices, and asserts that no accessibility violations exist.
Axe returns a detailed report with each violation having:
id: unique identifier (e.g., color-contrast)description: clear explanation of the rulehelp: guidance on how to fix the issuenodes: list of DOM elements failing the ruleimpact: severity: critical, serious, moderate, minorA common failure is misinterpreting impact and violation.severity. The impact field is a string ("critical", "serious", etc.), while the severity field is an object with value and name. The test script must check violation.impact and violation.severity.value.
For example:
for (const violation of violations) {
console.error(`Accessibility violation: ${violation.id}`);
console.error(`Description: ${violation.description}`);
console.error(`Impact: ${violation.impact}`);
for (const node of violation.nodes) {
console.error(`Element: ${node.html}`);
console.error(`Selector: ${node.target}`);
}
}
A frequent oversight is that aria-labelledby is not properly linked to an element, causing screen readers to announce only the label text but not the full context.
Interactive components require special attention. A modal dialog must announce its opening, and a tabbed interface must update the aria-selected and aria-controls attributes correctly.
Use the following test pattern:
aria-expanded, aria-selected, aria-checked, and aria-current are updated.aria-live regions announce changes.For instance, in a tabbed interface:
test('tabs are accessible', async ({ page }) => {
await page.goto('/tabs');
const tabList = page.locator('[role="tablist"]');
const tab1 = page.locator('[role="tab"][aria-selected="true"]');
const tab2 = page.locator('[role="tab"][aria-selected="false"]');
await expect(tab1).toHaveAttribute('aria-selected', 'true');
await expect(tab2).toHaveAttribute('aria-selected', 'false');
await tab2.click();
await expect(tab1).toHaveAttribute('aria-selected', 'false');
await expect(tab2).toHaveAttribute('aria-selected', 'true');
});
A common failure is that aria-expanded is set on a button but not updated when the content is revealed.
Focus must be managed across modals, dialogs, and complex controls.
When a modal opens:
aria-expanded="true".aria-hidden="true".Test this with:
test('modal dialog is accessible', async ({ page }) => {
await page.goto('/modal');
const trigger = page.locator('[aria-controls="modal"]');
await trigger.click();
const modal = page.locator('#modal');
await expect(modal).toBeVisible();
// Focus is on modal
await expect(page.locator('#modal')).toBeFocused();
// Background is hidden
await expect(page.locator('body')).toHaveAttribute('aria-hidden', 'true');
// Escape closes modal
await page.keyboard.press('Escape');
await expect(modal).toBeHidden();
});
A silent failure is that the focus is not set on the modal, leaving the user stranded in the background.
Color contrast, text size, and visual hierarchy are critical for users with low vision or cognitive load.
Use the wcag21 contrast ratio algorithm with a minimum of 4.5:1 for normal text and 3:1 for large text.
A test script validates contrast using the color-contrast rule from axe-core. The test checks that:
test('text has sufficient color contrast', async ({ page }) => {
await page.goto('/blog');
const contrastResults = await page.evaluate(() => {
const axeResults = axe.run(document);
return axeResults.violations.filter(v => v.id === 'color-contrast');
});
for (const violation of contrastResults) {
for (const node of violation.nodes) {
const contrastRatio = node.computedStyle.colorContrast;
expect(contrastRatio).toBeGreaterThanOrEqual(4.5);
}
}
});
A frequent issue is that background images are not properly considered: the contrast calculation uses only the immediate background, not the full visual layer.
Test that the UI remains accessible when users zoom to 200% and when screen size changes.
Use the following steps:
page.setViewportSize({ width: 320, height: 600 }).A common failure is that text overflows containers but no overflow is visible, and that font-size is set in px rather than rem or em, making responsive scaling ineffective.
Each accessibility test must produce a report that includes:
id, description, and help.A standard report format includes:
{
"testRunId": "2024-05-15-12-00",
"environment": {
"browser": "Chrome",
"platform": "macOS",
"screenReader": "VoiceOver",
"zoom": "200%",
"colorScheme": "light"
},
"results": [
{
"id": "color-contrast",
"description": "Text and background have insufficient color contrast.",
"help": "Ensure text has at least 4.5:1 contrast ratio.",
"nodes": [
{
"target": ["#main-nav a"],
"html": "<a href=\"/about\">About</a>",
"computedStyle": {
"color": "#ffffff",
"backgroundColor": "#333333",
"colorContrast": 4.7
}
}
]
}
]
}
This format enables teams to track regressions, prioritize fixes, and validate that accessibility improvements have been implemented.
alt text on images: screen readers announce “image” but not the content.aria-labelledby: the label is not read when the element gains focus.role values: a button is role="button" but not aria-pressed when toggled.These issues are often discovered only after deployment, when users with assistive technologies report problems that were not caught in testing.
Accessibility testing is not a one-time task. It is a continuous process, embedded in the development lifecycle, ensuring that every user, regardless of ability, can interact with the product with confidence and ease.
This page was rewritten on 18 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy