How to Test Accessibility With Playwright
Playwright is one of the strongest platforms for accessibility testing because it controls all three major rendering engines (Chromium, Firefox, WebKit) through a single API. This matters for accessibility because screen readers behave differently across browsers, and the browser's accessibility tree (the data structure that assistive technology reads) varies between engines. Testing accessibility in Playwright across all three engines reveals issues that single-browser testing misses, such as ARIA attributes that Chromium interprets correctly but WebKit handles differently.
Install @axe-core/playwright
The @axe-core/playwright package is the official axe-core integration for Playwright, maintained by Deque Systems. Install it as a development dependency alongside your existing Playwright setup. If you are using npm, run npm install --save-dev @axe-core/playwright. For yarn, use yarn add --dev @axe-core/playwright. For pnpm, use pnpm add -D @axe-core/playwright.
The package depends on axe-core, which it installs automatically. You do not need to install axe-core separately. The package provides the AxeBuilder class, which creates an axe analyzer instance attached to a Playwright Page object. AxeBuilder handles injecting the axe-core library into the page, running the analysis, and returning structured results.
Verify the installation by importing AxeBuilder in a test file: const { AxeBuilder } = require('@axe-core/playwright') for CommonJS, or import { AxeBuilder } from '@axe-core/playwright' for ES modules and TypeScript. If the import resolves without errors, the package is installed correctly.
Write Your First Accessibility Test
A basic Playwright accessibility test navigates to a page, creates an AxeBuilder instance, runs the analysis, and asserts that no violations were found. Here is the structure in TypeScript using Playwright Test:
Import test and expect from @playwright/test, and import AxeBuilder from @axe-core/playwright. In a test block, use page.goto() to navigate to the URL you want to test. Create a new AxeBuilder({ page }) instance, call .analyze(), and store the result. The result object contains a violations array; assert that violations.length equals zero.
The analyze() method injects axe-core into the page, runs the full rule set against the rendered DOM, and returns a results object. The violations array contains objects describing each failure, including the rule ID, the WCAG criteria violated, the impact level (critical, serious, moderate, minor), the affected HTML elements, and a help URL linking to Deque's documentation for that specific rule.
If violations are found and the test fails, the raw violations array is not immediately readable in test output. Add a formatting step that maps each violation to a human-readable string showing the rule ID, impact, description, and the affected element's HTML. Attach this formatted string to the assertion message so the test failure output tells you exactly what is wrong and where.
Configure Rules and Exclusions
AxeBuilder provides methods to customize which rules run and which page areas are analyzed. The withTags() method filters rules by WCAG conformance level. Passing ['wcag2a', 'wcag2aa'] runs only WCAG 2.0 Level A and AA rules. Passing ['wcag21aa'] runs WCAG 2.1 Level AA rules specifically. For most projects, ['wcag2a', 'wcag2aa', 'wcag21aa'] provides the complete WCAG 2.1 Level AA rule set, which is the standard required by most accessibility laws.
The disableRules() method excludes specific rules by their axe-core rule ID. This is useful when you have known issues that are tracked and scheduled for remediation but should not block CI builds in the meantime. For example, disableRules(['color-contrast']) would skip contrast checks. Use this sparingly and track every disabled rule with a corresponding issue ticket and remediation deadline.
The exclude() method removes specific page regions from analysis. Pass a CSS selector to exclude elements that are not your responsibility, such as third-party widgets, embedded iframes from external services, or legacy components scheduled for replacement. Common exclusions include third-party chat widgets, embedded social media feeds, and advertising iframes whose accessibility you cannot control.
The include() method limits analysis to specific page regions. If you only want to test a particular component or section, pass its CSS selector to include(). This is useful for component-level testing in design systems, where you want to verify the accessibility of a component in isolation without noise from the test page's scaffolding.
Handle and Report Violations
The axe results object returned by analyze() contains four arrays: violations (definite failures), passes (elements that passed checks), incomplete (elements that need human review), and inapplicable (rules that did not apply to the page). For automated testing, violations is the primary array. Each violation object contains the rule id, help text describing the issue, a helpUrl linking to documentation, impact severity, and a nodes array listing each affected element.
Each node in the violations array includes the element's HTML snippet, its CSS selector path, a failure summary explaining why it failed, and any related elements involved in the failure (such as the associated label element for a label-related failure). This information is sufficient to locate the element in your source code and understand what needs to change.
For better test output, create a helper function that formats violations into a readable report. Map each violation to a string containing the rule name, impact level, number of affected elements, and the HTML of the first few affected elements. Join these strings with line breaks and include the total violation count. Use this formatted string as the assertion message so failed tests produce actionable output rather than a generic "expected 0 to equal 15" message.
For advanced reporting, axe-core supports custom result reporters. You can write violations to a JSON file for post-processing, generate HTML reports using community packages like axe-html-reporter, or send results to accessibility tracking platforms through their APIs. Deque's axe DevTools includes a reporter that uploads results to a centralized dashboard for trend tracking across builds.
Add Accessibility Checks to Existing Tests
The highest-value approach is adding accessibility scans to your existing functional tests rather than creating separate accessibility-only tests. When your login test navigates to the login page, fills in credentials, and verifies successful authentication, add an AxeBuilder scan after the login page loads and another after the authenticated dashboard appears. This tests accessibility in the same states and flows your users actually encounter, including post-login states that require authentication to reach.
Add scans after significant state changes within a page. If a test opens a modal dialog, scan the page with the modal open to verify the modal's accessibility. If a test triggers form validation errors, scan after the errors appear to verify that error messages are properly associated and announced. If a test expands an accordion section, scan with the section expanded to test the revealed content.
Create a reusable helper function that performs the scan, formats violations, and asserts zero violations. Call this helper at strategic points in each test, keeping the code addition to a single line per scan. This pattern makes accessibility testing a natural part of your test workflow rather than a separate effort that requires its own test files and maintenance.
Consider running accessibility scans in only one browser project to avoid tripling your test execution time. axe-core rules test HTML and ARIA structure, which is typically identical across browsers. Running the accessibility scan only in the Chromium project and running functional tests across all three browsers is a reasonable optimization that catches the same accessibility issues without the performance cost of triple scanning.
Run in CI/CD
Playwright accessibility tests run in CI/CD identically to any other Playwright test. If you already run Playwright tests in GitHub Actions, GitLab CI, Jenkins, or another CI platform, accessibility tests execute as part of the same test suite with no additional configuration. The axe-core library is bundled with the @axe-core/playwright package, so no external service or API key is needed.
For teams adding accessibility tests to an existing project with violations, use a threshold approach. Count your current violations, set that count as the passing threshold, and ratchet it down as you fix issues. You can implement this by storing the expected violation count in a configuration file and comparing the actual count against it in your test assertion, failing only when the actual count exceeds the expected count.
Configure your CI to run accessibility tests on every pull request, not just on merges to the main branch. This gives developers immediate feedback about accessibility regressions they introduced, while the code is fresh in their minds and the fix is usually straightforward. Pull request comments that highlight specific accessibility failures, including the element and the WCAG criterion, help developers learn accessibility requirements through their daily workflow.
Store accessibility test artifacts (HTML reports, JSON results) as CI build artifacts. This creates an audit trail showing your accessibility posture over time, which is valuable for both internal tracking and demonstrating compliance effort if questions arise. Lighthouse CI provides built-in score tracking over time if you prefer its reporting format.
Advanced Patterns
For sites with complex authentication, use Playwright's storageState feature to save authentication cookies and local storage after a one-time login, then load that state in accessibility tests to skip the login flow. This lets you test authenticated pages efficiently without repeating login steps in every test.
For single-page applications where content loads dynamically, wait for the page to stabilize before running the accessibility scan. Use Playwright's waitForLoadState('networkidle') or wait for a specific element to appear before calling analyze(). Scanning too early may miss dynamically rendered content or catch the page in a loading state where elements have not yet received their final attributes.
For component libraries and design systems, create a dedicated test file that renders each component in isolation using Playwright's component testing or by navigating to a Storybook instance, then runs an accessibility scan on each component. This catches accessibility issues at the component level before they propagate to multiple pages across your application.
Combine accessibility testing with visual regression testing for comprehensive coverage. After running an axe-core scan, take a Playwright screenshot and compare it against a baseline. This combination catches both structural accessibility violations (via axe-core) and visual accessibility issues like overlapping text, clipped focus indicators, and layout problems at different zoom levels (via visual comparison).
Install @axe-core/playwright, add AxeBuilder scans to your existing functional tests after page loads and state changes, and run them in CI on every pull request. This catches the most common WCAG violations automatically, at the point in development where they are cheapest to fix.