Accessibility Testing

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.

Planning and Scoping Accessibility Tests

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%).

Running Manual Accessibility Tests

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:

Keyboard Navigation

To verify keyboard navigation, use the following pattern:

  1. Tab through all interactive elements in the page.
  2. Confirm that focus order matches visual flow.
  3. Test all keyboard interactions: space to activate, enter to submit, arrow keys for menus and sliders, and escape to close modals.

The test is considered complete when the following conditions are met:

Screen Reader Verification

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.

Automating Accessibility Tests with Axe

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 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.

Running Axe in CI

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.

Handling Axe Violations

Axe returns a detailed report with each violation having:

A 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.

Testing Dynamic and Interactive UIs

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.

Testing ARIA States and Properties

Use the following test pattern:

  1. Navigate to the component using keyboard.
  2. Verify that aria-expanded, aria-selected, aria-checked, and aria-current are updated.
  3. Confirm that 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.

Testing Focus Management

Focus must be managed across modals, dialogs, and complex controls.

When a modal opens:

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.

Validating Visual and Cognitive Accessibility

Color contrast, text size, and visual hierarchy are critical for users with low vision or cognitive load.

Testing Color Contrast

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.

Testing Text Resizing and Responsive Layouts

Test that the UI remains accessible when users zoom to 200% and when screen size changes.

Use the following steps:

  1. Set viewport to 320px wide.
  2. Zoom page to 200% using page.setViewportSize({ width: 320, height: 600 }).
  3. Verify that:

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.

Documenting and Reporting Accessibility Issues

Each accessibility test must produce a report that includes:

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.

Common Pitfalls and Silent Failures

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.