mrkeyoor.com_
Sun 20 Sept 02:40 UTC
npmTestingupdated 19 Sept 2026

axe-core review

axe-core 4.13.0 examines a live HTML document and returns four separate result sets: violations, passes, checks that need a person to decide, and rules that did not apply. It is the engine used inside browser tests, not a claim of WCAG certification. This release understands ARIA values exposed through ElementInternals, accepts the sectionheader and sectionfooter roles, adds Swedish output, and revises several ARIA and contrast decisions. Our complete browser import reached 589.7 KB minified and 160.6 KB gzipped, which makes test-only loading the sensible default.

63.5Mdownloads / wk
Verdict

axe-core 4.13.0 installed in 0.6 seconds with zero audit findings, but our full browser import was 160.6 KB gzipped, so it belongs in browser tests rather than shipped application code. Install it when the team will review incomplete cases and keep manual accessibility testing in the release process.

We installed it

Lab card: what happened when we installed axe-coreScreenshot of axe-core documentation
Install✓ · 0.6s1 package on disk · 4 MB
ImportESM import works · require() works · CommonJS package
Browser160.6 KBgzipped (589.7 KB minified), bundled with esbuild
TypesTypeScript types bundled
Known vulns00 critical · 0 high · 0 moderate · 0 low (npm audit)

Answers from our run

Does axe-core install cleanly?

Yes. In a fresh container with an empty cache, npm install axe-core finished in 0.6s, leaving 1 package and 4 MB on disk. npm audit reported no known vulnerabilities.

How much does axe-core add to a browser bundle?

160.6 KB gzipped (589.7 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.

Does axe-core work with both ESM and CommonJS?

Yes. Both import 'axe-core' and require('axe-core') worked in Node 22 in our run. The package is published as CommonJS.

Does axe-core include TypeScript types?

Yes, type declarations ship inside the package, so no @types install is needed.

axe-core or @axe-core/playwright: which should you use?

@axe-core/playwright: Choose the official adapter when Playwright is already your runner and you want injection plus a chainable scan builder. axe-core 4.13.0 installed in 0.6 seconds with zero audit findings, but our full browser import was 160.6 KB gzipped, so it belongs in browser tests rather than shipped application code.

When should you not use axe-core?

You need automation to prove conformance. Deque says the engine finds 57% of WCAG issues on average, and its incomplete results explicitly require manual review.

API stability4/5The 4.x API still revolves around axe.run, a context, scan options, and the same four result arrays. That call shape is dependable, but the findings are intentionally less fixed than the JavaScript surface. Deque's compatibility rules allow minor releases to add checks or update standards data, and the 4.13.0 release tells users to expect issue counts to change after its ARIA and ElementInternals work.
Docs4/5The official API reference documents contexts, nested frame targets, open shadow roots, runOnly, resultTypes, allowedOrigins, reporters, result objects, and performance tuning with code. The README also states the 57% automation estimate and the JSDOM contrast limit. A team still has to combine the API reference, rule catalog, browser notes, and compatibility policy before it can define a defensible CI gate.
Maintenance5/5GitHub shows the unarchived repository was pushed on 2026-08-25 and currently has 430 open issues and pull requests. Release 4.13.0 arrived on 2026-08-05 with ElementInternals support, two new ARIA roles, a Swedish locale, standards corrections, and fixes touching contrast, image alternatives, selectors, and landmarks. The README commits to security updates for minor lines up to 18 months old.
Ecosystem5/5npm recorded 67,916,706 downloads from 2026-08-19 through 2026-08-25, and GitHub reports 7,446 stars. Official and third-party adapters put the engine into browser runners and component-test workflows without changing its result vocabulary. Our package check found bundled declarations plus working require() and ESM import paths, although the package still has no exports map.

Use it if

  • You run Playwright, Cypress, Webdriver, or another browser test runner and want accessibility failures beside functional test failures.
  • Your design system needs repeatable checks for names, labels, ARIA state, landmarks, contrast, frames, and open shadow roots.
  • Reports must include the affected HTML, nested selectors, impact, a failure summary, and a rule-specific help URL.
  • Someone on the team will triage the incomplete results and still test keyboard and screen-reader behavior by hand.
Skip it if

Setup reality

We installed axe-core 4.13.0 in a clean, unprivileged Node 22 Bookworm container. npm finished in 0.6 seconds, put one package on disk, and used 4 MB. The package has zero direct and peer dependencies, occupies 3,100 KB unpacked, bundles TypeScript declarations, and produced zero npm audit findings. It is CommonJS without an exports map; both require() and ESM import worked in our sandbox. A full esbuild import measured 589.7 KB minified and 160.6 KB gzipped.

No account or configuration file is needed, but axe requires a rendered DOM. A plain Node import gives it no page to inspect. Browser adapters such as @axe-core/playwright inject the script after navigation. Each iframe needs its own copy. For cross-origin frames, configure allowedOrigins inside every participating document; a parent page cannot inspect another origin by itself. JSDOM lacks enough layout information for the color-contrast rule.

Run the scan after the exact UI state is visible and stable. The result has violations, incomplete, passes, and inapplicable arrays. Incomplete is work, not noise: it includes cases where layout, imagery, or another technical limit prevented a decision. WCAG tags do not inherit lower levels, so a wcag2aa selection must also name wcag2a if both are part of the policy.

Version 4.13.0 turns on ElementInternals handling, so custom elements can gain or lose findings after this upgrade. Large pages also spend time calculating unique selectors, especially around contrast. resultTypes: ['violations'] keeps full node detail for violations and limits detail in the other groups. Nested frame and shadow-root targets are arrays; flattening them into one CSS selector breaks the path.

Patterns

Inspect the visible document scan-current-document

const result = await axe.run();
console.log(result.violations);
console.log(result.incomplete);

axe.run reads the browser's current DOM. Call it only after the state under test is visible, and send incomplete items to manual review.

Run named WCAG levels select-wcag-tags

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

A 2.0 AA tag does not pull in 2.0 A. Name every standards level that your gate is meant to cover.

Check one application region limit-scan-context

const result = await axe.run({
  include: ['main'],
  exclude: ['[data-owned-by-vendor]'],
});

The excluded subtree produces no findings. Keep the exception narrow and record who owns the omitted UI.

Turn violations into a test failure fail-ci-on-violations

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

if (violations.length > 0) {
  const message = violations.flatMap((rule) =>
    rule.nodes.map((node) => `${rule.id}: ${JSON.stringify(node.target)}\n${rule.helpUrl}`)
  ).join('\n\n');
  throw new Error(message);
}

Nested frames and shadow roots produce structured target arrays. JSON output preserves that path when one flat CSS selector cannot.

Omit one rule from one scan disable-rule-locally

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

A per-run rule option stays local. axe.configure changes later scans in the same frame until configuration is reset.

Add an accessibility assertion to Playwright test-with-playwright

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

test('checkout has no automatic violations', async ({ page }) => {
  await page.goto('/checkout');
  const result = await new AxeBuilder({ page })
    .withTags(['wcag2a', 'wcag2aa'])
    .analyze();
  expect(result.violations).toEqual([]);
});

@axe-core/playwright handles script injection for this runner. The assertion covers automatic violations, while incomplete cases need a separate review path.

Keep detailed nodes only for violations reduce-selector-work

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

This option returns full violation nodes and at most one node per rule in the other result types, reducing selector calculation on large documents.

Inspect rules assigned to a tag list-rule-metadata

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

axe.getRules exposes the installed 4.13.0 catalog. Use that list during upgrades instead of assuming a tag maps to a fixed set forever.

Run an explicit starter set pin-rule-baseline

const result = await axe.run({
  runOnly: {
    type: 'rule',
    values: ['button-name', 'image-alt', 'label', 'link-name', 'color-contrast'],
  },
});

An explicit list avoids surprise additions in CI, but every unnamed rule is skipped. Reconcile the list with the catalog at each upgrade.

Select a form within an iframe target-inside-frame

const result = await axe.run({
  fromFrames: ['iframe#payment', 'form'],
});

Cross-origin documents also need axe and compatible allowedOrigins configuration inside each frame before their results can reach the parent.

Permit one cross-origin frame configure-frame-origins

axe.configure({
  allowedOrigins: ['<same_origin>', 'https://payments.example.com'],
});

Apply the same origin policy in every participating frame. The API warns against <unsafe_all_origins>, and an empty array blocks all frame communication.

Apply runtime locale messages change-result-language

import de from 'axe-core/locales/de.json' with { type: 'json' };

axe.configure({ locale: de });
const result = await axe.run();

The locale object must match the package's locale schema. axe.resetLocale() restores the earlier language without clearing unrelated rule configuration.

Alternatives

PackageRegistryPick it when
@axe-core/playwrightnpmChoose the official adapter when Playwright is already your runner and you want injection plus a chainable scan builder.
pa11ynpmChoose it for command-line URL checks and standard reports without building a browser-test wrapper.
lighthousenpmChoose it when accessibility is one part of a wider page audit that also covers performance and SEO.

More testing guides

pytest · chai · vitest · jsdom · playwright · coverage · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.