mrkeyoor.com_
Thu 06 Aug 00:59 UTC
npmTestingupdated 05 Aug 2026

axe-core

axe-core is the accessibility rules engine that most automated a11y tooling is built on. You load it into a page, call axe.run(), and get back four arrays: violations, passes, incomplete (things a machine cannot decide), and inapplicable. Each violation names the rule, an impact level from minor to critical, a link to Deque University explaining the fix, and CSS selectors for every failing element. The rules are tagged by standard, so you can ask for only WCAG 2.1 AA, or only Section 508, or add Deque's best-practice checks. It is a browser-side library with zero runtime dependencies, which is why Lighthouse, the axe browser extensions, and the Playwright, Cypress, and Jest integrations all wrap this same engine rather than writing their own.

Verdict

The best automated accessibility engine available and the right foundation for a CI gate, as long as everyone understands it catches roughly half of WCAG issues and never replaces manual and assistive-technology testing. Unless you are building tooling, use one of the official framework wrappers rather than this package directly.

API stability5/5axe.run, axe.configure, and the results shape have been unchanged through the whole 4.x line since 2020, and the project publishes a backwards-compatibility document stating what kinds of change each release type may introduce.
Docs4/5doc/API.md is a genuinely complete reference with context, options, results, and performance guidance, plus a full rule-descriptions list and honest notes about jsdom and browser support; the downside is that it is a pile of markdown files in the repo rather than a searchable site.
Maintenance5/5Pushed August 2026 with 4.13.0 released the same week, funded and staffed by Deque Systems, a published release cadence of every three to five months, and 18 months of security support per minor line; the tracker carries 413 open issues (421 counting PRs), which is large but active.
Ecosystem5/5Around 64 million weekly downloads and the engine underneath Lighthouse's accessibility category, the axe browser extensions, and the official Playwright, Cypress, WebdriverIO, and React integrations.

Use it if

  • You want accessibility regressions caught in CI alongside your existing end-to-end tests, on the rendered DOM rather than on source files
  • You need results tied to a named standard: runOnly with tags like wcag21aa or EN-301-549 gives you exactly the rule set your compliance target requires
  • You are building a tool that reports accessibility problems and want an engine with stable rule IDs, help URLs, and impact levels rather than writing checks yourself
  • You test components in isolation and want a per-component gate: jest-axe wraps this engine and fails a unit test on rendered markup
Skip it if

Setup reality

npm install --save-dev axe-core gives you a single UMD script with no dependencies, which is the easy part. The friction is everything around getting it into the page. Because it runs inside the browser, you have to inject axe.min.js into every frame you want tested, and axe.configure() does not propagate into iframes, so any rule tweaks must be applied in each frame separately. Cross-origin frames need an allowedOrigins configuration on both sides or results simply come back missing. In driver-based setups such as Selenium and Puppeteer, the project recommends the runPartial and finishRun pair rather than plain axe.run, which is exactly the sort of plumbing the official integrations exist to hide. The other real cost is upgrade behaviour: a new minor lands every three to five months and usually adds rules, so a build that was green on 4.12 can fail on 4.13 with no change to your code, and security fixes only go back 18 months of minor lines. Budget time for triage after every bump, and expect a first run on an existing app to produce a large violation list that needs to be turned into a baseline rather than a blocking gate on day one.

Patterns

Scan the whole documentrun-on-page

<script src="node_modules/axe-core/axe.min.js"></script>
<script>
  axe.run().then(results => {
    console.log(results.violations.length, 'violations');
    console.log(results.incomplete.length, 'need review');
  });
</script>

With no callback axe.run returns a promise, but it does not polyfill one. It must be loaded inside every frame you want covered; a script tag in the top document alone will not reach an iframe.

Run only the rules for one conformance targetlimit-to-standard

const results = await axe.run({
  runOnly: {
    type: 'tag',
    values: ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22aa']
  }
});

Tags do not nest: asking for wcag2aa alone does not include wcag2a, so list every level you care about. Leaving runOnly off also runs Deque's best-practice rules, which are opinions rather than WCAG requirements and will inflate your violation count.

Include and exclude parts of the pagescope-the-scan

const results = await axe.run({
  include: ['main'],
  exclude: ['.ad-banner', '[data-third-party]']
});

Excluding third-party embeds is usually the difference between a usable report and noise you cannot fix. You can also pass a plain element, a NodeList, or a selector string as the first argument when you do not need both lists.

Turn violations into a useful CI failurefail-a-test-readably

const { violations } = await axe.run();

if (violations.length) {
  const report = violations
    .map(v => `${v.impact} ${v.id}: ${v.help}\n  ${v.nodes.map(n => n.target.join(' ')).join('\n  ')}\n  ${v.helpUrl}`)
    .join('\n\n');
  throw new Error(`Accessibility violations:\n\n${report}`);
}

Printing helpUrl in the failure is what makes the gate survive contact with a team: every rule links to a Deque University page explaining the fix. node.target is an array because each entry is one frame level deep.

Turn off a rule you have consciously accepteddisable-rules

const results = await axe.run({
  rules: {
    'color-contrast': { enabled: false },
    'region': { enabled: false }
  }
});

Prefer disabling per run over axe.configure, which is global and does not cross iframe boundaries. Record why in a comment; a disabled rule with no reason gets copied forward forever.

Wire it into a Playwright testplaywright-integration

import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

test('checkout page has no a11y violations', async ({ page }) => {
  await page.goto('/checkout');
  const results = await new AxeBuilder({ page })
    .withTags(['wcag2a', 'wcag2aa'])
    .exclude('#stripe-frame')
    .analyze();
  expect(results.violations).toEqual([]);
});

AxeBuilder injects axe-core into every frame and uses runPartial internally, which is the pattern the project recommends for driver-based runs. Doing this by hand with page.evaluate misses iframes.

Assert on a single component in Jestcomponent-unit-test

import { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';

expect.extend(toHaveNoViolations);

test('Button is accessible', async () => {
  const { container } = render(<Button>Save</Button>);
  expect(await axe(container)).toHaveNoViolations();
});

This runs in jsdom, where color-contrast silently does nothing because there is no layout or painting. Treat component-level checks as a cheap first pass and keep a browser-based run for the real coverage.

Cut analysis time on very large pagesspeed-up-large-pages

const results = await axe.run({
  resultTypes: ['violations']
});

The docs note that pages over about 50,000 elements can take more than 10 seconds, largely because axe computes a unique selector for every result. resultTypes limits the full detail to violations and returns a single sample for passes, incomplete, and inapplicable.

Target content inside frames or a shadow rootframes-and-shadow-dom

// only the form inside the payment iframe
await axe.run({ fromFrames: ['iframe#payment', 'form'] });

// skip comment bodies inside a shadow host
await axe.run({
  exclude: { fromShadowDom: ['.commentsShadowHost', '.commentBody'] }
});

Cross-origin frames need axe.configure({ allowedOrigins: [...] }) called inside each frame, otherwise results come back quietly incomplete. Do not use the <unsafe_all_origins> wildcard on anything but a local fixture.

Do something with the incomplete resultsreview-incomplete

const { incomplete } = await axe.run();

for (const item of incomplete) {
  console.warn(`needs review: ${item.id} on ${item.nodes.length} node(s)`);
  for (const node of item.nodes) console.warn('  ', node.html);
}

Incomplete items are where axe could not decide, commonly contrast over a background image or an element it could not resolve. Most teams throw this array away, which is where a real chunk of the misses live.

Pin an explicit rule list so upgrades cannot break CIpin-rule-set

const RULES = ['button-name', 'image-alt', 'label', 'link-name', 'color-contrast'];

const results = await axe.run({
  runOnly: { type: 'rule', values: RULES }
});

Minor releases add new rules every few months, so a tag-based run can fail after a dependency bump with no code change. An explicit rule list trades coverage for a stable gate, which is a reasonable first step when adopting axe on an existing product.

Inspect what rules exist and what they coverlist-available-rules

const rules = axe.getRules(['wcag22aa']);
console.table(rules.map(r => ({ id: r.ruleId, help: r.description })));

// everything, with tags
console.log(axe.getRules().length);

Useful when writing your own report or deciding on a baseline. Pass an array of tags to filter; with no argument you get every rule the engine knows, including experimental ones that are disabled by default.

Alternatives

PackageRegistryPick it when
@axe-core/playwrightnpmYou run Playwright and want the same engine with frame injection, tag filtering, and result handling already wired up.
jest-axenpmYou want per-component accessibility assertions in unit tests and can live with jsdom's limits, notably no colour contrast checking.
pa11ynpmYou want a command-line crawler and CI reporter over a list of URLs rather than a library to embed in your own tests.
cypress-axenpmYour end-to-end suite is Cypress and you want a cy.checkA11y() command that fails the spec on violations.