View all services
Talk to QA Advisor
Browse the Knowledge Hub211 resources
/Tools Library/axe-core
Tools Library · We run this

axe-core review: accessibility testing tutorial and pitfalls

axe-core is Deque’s open-source accessibility rules engine. It runs inside a rendered page, checks the DOM against WCAG 2.0 to 2.2 and best-practice rules, and returns violations, passes and items that need human review. It is built to be embedded in existing test suites, and it powers Lighthouse’s accessibility audits and Deque’s commercial axe products. Version 4.14 shipped on 5 October 2026.

QAble runs this tool on client engagements. The assessment below is from that experience.

last reviewed
13 October 2026
version covered
axe-core 4.14
licence
Open source

Reviewed by Viral Patel

Co-Founder, QAbleLast reviewed 13 October 2026

At a glance

axe-core at a glance

Official site
Vendor
Deque Systems
Category
Accessibility testing
Licence
Open source (MPL 2.0)
Platforms
Browser, Node.js
Integrates with
Playwright, Cypress, Selenium, Jest
First released
2015
Version reviewed
axe-core 4.14

Verdict

Our verdict on axe-core

Strengths

  • Designed for zero false positives, so a reported violation is almost always a real defect worth fixing
  • Runs inside the page your test already opened, so modals, menus and error states can be scanned
  • Tags map rules to WCAG 2.0, 2.1, 2.2, Section 508, EN 301 549 and Trusted Tester
  • Each violation carries a selector, an HTML snippet, a fix summary and a help link
  • Free under MPL 2.0, with official wrappers for Playwright, WebdriverIO, Puppeteer and Selenium

Weaknesses

  • Automated rules cannot judge meaning, keyboard flow or screen reader experience, so manual testing remains essential
  • WCAG 2.2 rules such as target-size are disabled unless you request the wcag22aa tag
  • Wrapper packages pin an older axe-core minor, so new rules arrive in your suite later
  • Uncertain results land in incomplete, which teams often ignore and so lose real findings

Best for

Catching the mechanical accessibility failures on every build, inside the end-to-end or component suite a team already runs: missing form labels, images without alternative text, insufficient contrast, invalid ARIA, duplicate landmarks, buttons with no accessible name. Adding it to a Playwright, WebdriverIO or Selenium suite takes an afternoon, it scans whatever state a test reaches, and its findings are precise enough that developers fix them rather than argue with them. For the effort involved it is the best-value accessibility work we know of.

Not for

Demonstrating conformance or replacing an audit. Deque’s own figure is that axe-core finds 57% of WCAG issues on average, which is a count of issues rather than success criteria, and the remainder depends on human judgement: whether alternative text is meaningful, whether focus order makes sense, whether a screen reader user can complete the task. A clean axe run on a page says nothing about keyboard traps that only appear mid-journey, or about content that is technically labelled and practically useless.

Setup cost

Very low for the mechanics: one dev dependency and a few lines in an existing test. The decisions take longer. Agree which tags define your standard, usually WCAG 2.x A and AA plus wcag22aa, decide whether best-practice rules fail the build or only report, and list the third-party widgets you cannot fix so they are excluded explicitly. Then decide which page states to scan, because a scan only sees what is rendered at that moment.

Maintenance burden

Low, with two recurring tasks. Minor releases arrive every three to five months and can add or enable rules, so unchanged pages can start failing after an upgrade; 4.14 enabled label-content-name-mismatch by default. Treat that as new findings, not as a broken tool. The second task is triage: incomplete results need a person, and known issues need tracking with fingerprints rather than blanket exclusions, or the suppression list quietly grows until the check means nothing.

Honest limitations

It only judges what is in the DOM at the moment of the scan, so dynamic states must be driven there by the test. The zero false positive design routes anything ambiguous to incomplete, so violations alone understate the problem. WCAG 2.2 and AAA rules are off unless you ask for them. The colour contrast rule does not work in JSDOM, so unit-level scans miss contrast entirely. And it cannot assess screen reader output, keyboard flow or cognitive load.

How it works

What is axe-core and how does it work?

An engine that runs inside the page

axe-core is a JavaScript file injected into the page under test. axe.run then executes in the page’s own context against the live, rendered DOM, computing roles, accessible names, visibility and on-screen colours the way the browser sees them. That is why it needs a real rendering engine to be accurate: the README notes limited JSDOM support, and the colour contrast rule does not work there at all. It also explains its reach. axe-core sees exactly one moment of one page, so anything the user only meets after an interaction, such as an open dialog or a validation message, has to be produced by your test before the scan runs.

Four outcomes rather than pass or fail

Results come back in four arrays. Violations are elements that definitely fail a rule. Passes are elements that definitely meet it. Incomplete, also called needs review, holds elements axe could not decide, for example text over a background image where contrast cannot be computed. Inapplicable lists rules with nothing to check on the page. Each violation carries an impact of minor, moderate, serious or critical. The split follows from Deque’s manifesto of zero false positives: rather than guess, the engine moves anything uncertain to incomplete. That makes violations trustworthy enough to fail a build on, and makes incomplete the list most teams forget to read.

Tags decide which rules run

Every rule carries tags: a WCAG version and level such as wcag2a or wcag21aa, a success criterion such as wcag111, best-practice for rules no criterion requires, and tags for Section 508, Trusted Tester, EN 301 549 and RGAA. With no options, axe runs every enabled rule, best practices included, and skips experimental ones. WCAG 2.2 rules and AAA rules ship disabled by default. Passing tags through runOnly changes the selection: the engine’s rule selection code runs every rule carrying a requested tag, including disabled ones, while still excluding experimental rules unless you ask for them. Requesting wcag2aa does not imply wcag2a; each level needs its own tag.

Frames, shadow DOM and wrapper packages

axe-core follows iframes by default and waits up to 60 seconds for each frame to answer, then merges the results. Elements inside open shadow roots are reported with a nested selector array, one entry per shadow boundary. axe.configure does not propagate into frames, which is one reason most teams use a wrapper. Deque’s @axe-core packages for Playwright, Puppeteer, WebdriverIO and WebdriverJS inject the engine into every frame, run it, and gather the results for you. Each wrapper is versioned to the axe-core minor it bundles: @axe-core/playwright 4.13 depends on axe-core 4.13.x, so a release such as 4.14 reaches wrapper users only when the wrapper catches up.

Strengths

What it does well

Accessibility checks inside the tests you already run

Official packages cover Playwright, Puppeteer, WebdriverIO, WebdriverJS, a CLI and React, with Deque integrations for Java Selenium and several Ruby tools, and a community package for Cypress. Scans therefore run in the same journeys, logins and data setups as functional tests, which costs far less than maintaining a separate accessibility crawl that cannot get past a login form.

Standards mapped through tags

One engine answers several compliance questions. Tags map rules to WCAG 2.0, 2.1 and 2.2 at A, AA and AAA, to Section 508, to the US Trusted Tester process, to EN 301 549 including its version 4 tags, and to France’s RGAA. A public sector project can scan to EN 301 549 while a product team scans to WCAG 2.2 AA, with the same code.

Findings a developer can act on

Each violation names the rule, its impact, a help text and a link to the rule’s page on Deque University, and lists every failing node with a CSS selector, the offending HTML and a failure summary that says what to change. Developers can usually fix a finding straight from the test output, which is what keeps accessibility checks from becoming a queue nobody owns.

Scoping to a component or region

Include and exclude contexts narrow a scan to one region, such as a newly opened menu, or keep out a third-party widget you cannot change. Scoped scans are faster on large pages and make ownership obvious, because a failure in the checkout form scan belongs to the checkout team. Playwright’s guide notes that exclude removes the element and all its descendants from every rule.

Custom rules, locales and reporters

axe.configure adds custom rules and checks, changes the reporter format, and applies a locale. Built-in locales include German, French, Japanese, Dutch and, since 4.14, Azerbaijani, so failure messages can match the language of the team reading them. Custom rules suit organisation-specific patterns, such as a design system component that must always carry a particular attribute.

A predictable release policy

The README commits to a minor release every three to five months, usually adding rules, and to security updates for minor lines up to 18 months old. Deprecated rules are disabled by default and removed only at the next major. That predictability lets a team schedule upgrades deliberately rather than discover new failures by accident.

Pricing

axe-core pricing and licensing

axe-core is free and open source under the Mozilla Public License 2.0, with nothing held back in the engine. Deque funds it through commercial products built on top: the axe DevTools browser extension in Free and Pro versions, and axe DevTools for Web, a bundle with CI integrations, linting and central reporting sold through its sales team. None of them are needed to use axe-core in tests. The real cost is the manual testing automation cannot replace.

  • Licence: MPL 2.0 for the engine; the bundled dependencies carry their own terms, listed in the repository’s third-party licence file.
  • Wrappers: the @axe-core packages for Playwright, Puppeteer, WebdriverIO, WebdriverJS, the CLI and React are free and maintained by Deque in a separate repository.
  • axe DevTools Extension: a Free version for page-by-page testing, and a Pro version bought online with a 14-day trial that adds guided tests and Jira integration.
  • axe DevTools for Web (Bundle): everything in Pro plus CI and API integrations and centralised reporting, priced through Deque’s sales team rather than a public list.
  • Hidden cost: manual and assistive technology testing for everything the engine cannot judge, plus time to triage incomplete results and maintain a known-issues list.

Checked against the vendor’s own pages on 13 October 2026. Pricing changes; confirm on the official site before buying.

Getting started

axe-core tutorial: getting started

Setup, step by step

  1. Start from a working Playwright Test project; the same pattern applies to WebdriverIO or Selenium wrappers.
  2. Install @axe-core/playwright as a dev dependency.
  3. Write a test that navigates to a page and waits for the content you want checked.
  4. Run AxeBuilder with the WCAG tags you have agreed, including wcag22aa if you target WCAG 2.2.
  5. Assert that the violations array is empty and run the test locally.
  6. Attach the full results to the test so failures can be read from the report.

Install

npm install --save-dev @axe-core/playwright
# Playwright Test itself, if the project does not have it yet
npm init playwright@latest

A first example

// tests/a11y/home.spec.ts
import { test, expect } from '@playwright/test'
import AxeBuilder from '@axe-core/playwright'

test('home page has no detectable WCAG A or AA violations', async ({ page }, testInfo) => {
  await page.goto('https://example.com/')
  // Scan only once the content you care about is rendered
  await expect(page.getByRole('main')).toBeVisible()

  const results = await new AxeBuilder({ page })
    // Each level needs its own tag; wcag22aa switches on WCAG 2.2 rules
    .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22aa'])
    .analyze()

  // Keep the whole result, including incomplete items, for review
  await testInfo.attach('axe-results', {
    body: JSON.stringify(results, null, 2),
    contentType: 'application/json',
  })

  expect(results.violations).toEqual([])
})

What trips people up first

Scanning too early, or scanning the wrong thing. AxeBuilder checks the page in its current state, so a scan straight after goto can run before client-side rendering finishes and report a clean page that was never really checked. Wait for a meaningful element first, as the snippet does. The second surprise is the first real run on an established application: dozens of violations from shared components. Resist disabling rules to get green. Fix the shared component once, which clears the violation on every page, exclude only what genuinely belongs to a third party, and track the rest with fingerprints.

In practice

Using axe-core in practice

One fixture that defines your standard

Put the agreed configuration in a single Playwright fixture so every scan uses the same tags and the same documented exclusions. When the standard changes, for example adding wcag22aa or EN 301 549 tags, it changes in one file. Keep exclusions specific and commented with the reason and an owner, because an exclusion hides every rule for that element and all its children. Tests then ask the fixture for a builder and add only what is specific to them, such as an include for the region under test.

// tests/a11y/axe-test.ts
import { test as base, expect } from '@playwright/test'
import AxeBuilder from '@axe-core/playwright'

type AxeFixture = { makeAxeBuilder: () => AxeBuilder }

export const test = base.extend<AxeFixture>({
  makeAxeBuilder: async ({ page }, use) => {
    await use(() =>
      new AxeBuilder({ page })
        .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22aa'])
        // Vendor chat widget: tracked with the vendor, owner: web team
        .exclude('#vendor-chat')
    )
  },
})
export { expect }

// tests/a11y/checkout.spec.ts
import { test, expect } from './axe-test'

test('payment form, including its error state', async ({ page, makeAxeBuilder }) => {
  await page.goto('/checkout/payment')
  await page.getByRole('button', { name: 'Pay now' }).click()
  await expect(page.getByRole('alert')).toBeVisible()

  const results = await makeAxeBuilder().include('form#payment').analyze()
  expect(results.violations).toEqual([])
})

Scanning the states users actually meet

A page load is the least interesting moment to scan. Most accessibility defects live in states reached by interaction: an open modal without a label, a menu whose items have no names, an inline error that is not announced, a step of a wizard that only exists after the previous one. Add scans at those points in existing functional journeys rather than writing separate accessibility-only tests that only visit URLs. A practical rule is one scan per distinct state: after the page settles, after opening each overlay, and after triggering validation. Scope each scan with include so the result is attributable and fast, and keep one full-page scan per template so landmark and heading rules still see the whole document.

Living with known issues without hiding new ones

On an established product the first runs will fail on existing defects. Playwright’s documentation recommends against snapshotting the whole violations array, because it contains rendered HTML that changes for unrelated reasons, and suggests a fingerprint of rule IDs and target selectors instead. Snapshot the fingerprint: the test then fails when a new violation appears or an existing one changes, while the known list is reviewed like any other code. As issues are fixed the snapshot shrinks, and the diff shows exactly which ones. Prefer this to disableRules, which switches a rule off everywhere and lets new instances in unnoticed.

// tests/a11y/fingerprint.ts
import type { AxeResults } from 'axe-core'

// Rule ID plus selectors: stable across copy and styling changes
export function violationFingerprints(results: AxeResults): string {
  const prints = results.violations.map((v) => ({
    rule: v.id,
    impact: v.impact,
    targets: v.nodes.map((n) => n.target),
  }))
  return JSON.stringify(prints, null, 2)
}

// In a spec:
// const results = await makeAxeBuilder().analyze()
// expect(violationFingerprints(results)).toMatchSnapshot('legacy-account-page.json')

Gating pull requests in GitHub Actions

Accessibility specs are ordinary Playwright tests, so they run in the same pipeline. Keep them in their own project or folder so they can run on every pull request even when the full end-to-end suite is sharded or nightly; a few dozen scoped scans take a minute or two. Upload the Playwright report on failure, since the attached axe JSON is what a developer needs to fix the finding. Some teams fail only on serious and critical impacts at first and report the rest; that is a reasonable start if the lower impacts have a dated plan, not a permanent exemption.

# .github/workflows/a11y.yml
name: accessibility
on: [pull_request]
jobs:
  axe:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version: 24
          cache: npm
      - run: npm ci
      - run: npx playwright install --with-deps chromium
      # Accessibility specs only; playwright.config reads BASE_URL into use.baseURL
      - run: npx playwright test tests/a11y --project=chromium
        env:
          BASE_URL: ${{ vars.PREVIEW_URL }}
      - uses: actions/upload-artifact@v7
        if: failure()
        with:
          name: a11y-report
          path: playwright-report

Incomplete results, upgrades and the manual layer

Route incomplete results somewhere a person sees them: a weekly review of the attached JSON, or a non-blocking check that lists them. They often contain real contrast failures over gradients and images. Upgrade axe-core on a schedule, reading the changelog for newly enabled rules; 4.14 enabled label-content-name-mismatch, which compares visible labels with accessible names and will flag icon buttons whose aria-label differs from their text. Where a wrapper lags the engine, AxeBuilder accepts an axeSource option with the source of a newer axe-core. Finally, pair the automated layer with keyboard-only and screen reader passes on each release’s changed journeys, since that is where the remaining issues are found.

Pitfalls

Common pitfalls

Assuming the default run covers WCAG 2.2

Why it happens: WCAG 2.2 rules such as target-size ship disabled by default, and a tag list copied from older guides stops at wcag21aa. The suite passes while touch targets fail the criterion.

The fix: Add wcag22aa to the tags you pass, which runs those rules even though they are disabled by default, and record the chosen tags in one shared fixture.

Passing only the AA tag

Why it happens: Tags are not cumulative. wcag2aa selects only rules tagged AA, so Level A rules such as missing alternative text or form labels are skipped.

The fix: List every level you need: wcag2a and wcag2aa, wcag21a and wcag21aa, plus wcag22aa. Check the selection once with axe.getRules on the tags.

A clean result from a page that had not rendered

Why it happens: The scan checks the DOM at the moment it runs. Immediately after navigation, a single-page application may show a loading shell with almost nothing to fail.

The fix: Wait for a meaningful element with a web-first assertion before scanning, and check that the passes array is not suspiciously short when a page reports no violations.

Ignoring incomplete results

Why it happens: Uncertain cases go to incomplete rather than violations, by design. Asserting only on violations means contrast over images and similar cases are never looked at.

The fix: Attach the full results to every test and give incomplete items a named reviewer and a regular slot, or surface them as a non-blocking report on each pull request.

Contrast checks in JSDOM unit tests

Why it happens: Jest with JSDOM has no layout or rendering, and the README states the colour contrast rule does not work under JSDOM. Unit-level scans therefore never fail on contrast.

The fix: Use JSDOM scans for structure and ARIA only, and run contrast-sensitive checks in a real browser through Playwright, WebdriverIO or component tests in a browser.

New failures after a minor upgrade

Why it happens: Minor releases add and enable rules. 4.14 enabled label-content-name-mismatch by default, and 4.12 deprecated the landmark-complementary-is-top-level rule, so old guides and configs drift.

The fix: Read the changelog before each upgrade, treat new findings as defects rather than regressions in the tool, and remove references to deprecated rule IDs, which are disabled and go at the next major.

Expecting the newest rules through a wrapper

Why it happens: Wrapper packages track an axe-core minor. @axe-core/playwright 4.13 depends on axe-core 4.13.x, so installing axe-core 4.14 alongside it changes nothing in the scan.

The fix: Check which axe-core version the wrapper resolves in your lockfile. If you need a newer engine before the wrapper updates, pass its source through the axeSource option.

Compared

How axe-core compares

ToolRules engineHow you run itInteractive statesLicence
axe-coreOwn rules, tagged WCAG 2.0 to 2.2Library inside your existing testsAny state your test reachesMPL 2.0
Pa11yHTML_CodeSniffer by default, axe optionalCLI or Node API on PuppeteerScripted actions such as click elementLGPL 3.0
Lighthouseaxe-core rules, weighted into a scoreDevTools, CLI or Node moduleSnapshot mode in user flowsApache 2.0
WAVEWebAIM’s own WAVE engineBrowser extension or paid APIThe page as rendered in your browserProprietary, API priced by credits

FAQ

Frequently asked questions

What is axe-core?

axe-core is Deque’s open-source accessibility rules engine. It runs inside a rendered page, checks the DOM against WCAG 2.0 to 2.2 and best-practice rules, and returns violations, passes and items that need human review. It is built to be embedded in existing test suites, and it powers Lighthouse’s accessibility audits and Deque’s commercial axe products. Version 4.14 shipped on 5 October 2026.

Is axe-core free?

Yes. axe-core is open source under the Mozilla Public License 2.0, and the official wrappers for Playwright, Puppeteer, WebdriverIO, WebdriverJS, the CLI and React are free as well. Deque sells separate products built on the engine, such as the axe DevTools Pro extension and the axe DevTools for Web bundle with CI integrations and central reporting, but none of them are required to run axe-core in your own test suite.

How much of WCAG can axe-core test automatically?

Deque states that axe-core finds 57% of WCAG issues on average, measured by the number of issues found rather than by success criteria. Anything it cannot decide goes to the incomplete list for manual review. Meaningful alternative text, logical focus order, keyboard operability across a journey and the screen reader experience all need human testing, so a clean axe run is a floor, not a conformance claim.

Does axe-core support WCAG 2.2?

Yes, through rules tagged wcag22aa, such as target-size for touch target size and spacing. Those rules are disabled by default until WCAG 2.2 is more widely adopted, so a default run or a tag list ending at wcag21aa will not execute them. Add wcag22aa to the tags you pass to runOnly or AxeBuilder.withTags and they run alongside the WCAG 2.0 and 2.1 rules.

What changed in axe-core 4.14?

Version 4.14 was released on 5 October 2026. It enables the label-content-name-mismatch rule by default, adds EN 301 549 version 4 tags to target-size, adds an Azerbaijani locale and fixes a long list of rule behaviours, including several to the label and contrast rules. Expect new findings on unchanged pages after upgrading, mostly on controls whose accessible name does not contain their visible text.

axe-core or Lighthouse for accessibility testing?

Lighthouse’s accessibility category runs axe-core rules under the hood and turns them into a weighted score, which is useful for a quick audit or a trend line. For gating builds, axe-core used directly is the better tool: you choose the tags, scan any state a test reaches, scope to regions, and assert on individual violations rather than on a number that can stay high while a critical failure exists.

How do I use axe-core with Cypress?

Deque does not ship an official Cypress wrapper; the common route is the community cypress-axe package, which injects axe-core with cy.injectAxe and runs it with cy.checkA11y, accepting the same context and options as axe.run. Check its declared Cypress support before upgrading Cypress: at the time of writing its peer dependency range lists Cypress versions up to 15.

Can axe-core be used from Java or Selenium?

Yes. Deque publishes axe-core integrations for Java Selenium through Maven, and for Ruby with Selenium, Capybara, Watir, RSpec and Cucumber, alongside the JavaScript packages for WebdriverJS and WebdriverIO. They inject the same engine into the browser your tests already drive and return the same results structure, so tag choices and triage practices carry across languages.

Alternatives

axe-core alternatives worth weighing

Pa11y

Command-line accessibility testing designed for CI, with dashboard and sitemap-crawling variants.

Lighthouse

Automated auditing for performance, accessibility, SEO and best practices, built into Chrome DevTools.

WAVE

Browser extension and online tool that overlays accessibility findings directly on the page.

Resources

Where to go next

Keep going

Prepare, or get it done

Ready

Picking the wrong tool costs more than picking late.

QAble advises on toolchain decisions and then implements them, with engineers who have run these tools on real delivery. Start with a free QA audit.

Talk to QA Advisor