Chat about this codebase

AI-powered code exploration

Online

Project Overview

axe-core is an open-source accessibility testing engine for web interfaces. It embeds a set of automated rules that detect common WCAG 2.x and ARIA violations, enabling developers and QA teams to catch accessibility issues early in their development and CI workflows.

Why axe-core Exists

  • Automate detection of accessibility defects during development and testing
  • Promote consistent implementation of WCAG guidelines
  • Integrate seamlessly into unit, integration, and end-to-end tests

Problems axe-core Solves

  • Manual reviews that miss edge cases or regressions
  • Inconsistencies across browsers and testing tools
  • Time-consuming audits by streamlining rule execution and reporting

Supported Runtimes & Browsers

Browsers

  • Chrome (Desktop & Headless)
  • Firefox (Desktop & Headless)
  • Safari
  • Microsoft Edge

Runtimes

  • Browser (via <script> or bundlers like Webpack/Rollup)
  • Node.js (for server-side rendering checks)
  • Headless automation (Puppeteer, Playwright)

Example: Running axe-core in Puppeteer

const puppeteer = require('puppeteer');
const { configureAxe, getAxeResults } = require('axe-puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('https://example.com');

  // Initialize axe-core in the page context
  await configureAxe(page).configure({ rules: [{ id: 'color-contrast', enabled: true }] });
  const results = await getAxeResults(page);

  console.log(results.violations);
  await browser.close();
})();

Community & Resources

License

axe-core is licensed under the Mozilla Public License, version 2.0. See LICENSE for full terms and contributor rights.

Getting Started

Get your first accessibility scan running in the browser, in Node, and in CI within minutes.

1. Installation

Via npm (Node & Bundlers)

Install axe-core as a dev-dependency:

npm install --save-dev axe-core

Via CDN (Browser)

Include the latest bundle from unpkg:

<script src="https://unpkg.com/axe-core@4/dist/axe.min.js"></script>

2. Browser Quick Start

Run an audit on page load and log violations:

<!doctype html>
<html>
  <head>
    <script src="https://unpkg.com/axe-core@4/dist/axe.min.js"></script>
  </head>
  <body>
    <!-- your page markup -->
    <script>
      document.addEventListener('DOMContentLoaded', () => {
        // Scan entire document
        axe.run()
          .then(results => {
            if (results.violations.length) {
              console.error('Accessibility violations:', results.violations);
            } else {
              console.log('No accessibility violations found');
            }
          })
          .catch(err => console.error('axe.run error:', err));
      });
    </script>
  </body>
</html>

3. Node.js Quick Start with Puppeteer

Use Puppeteer to load a page, inject axe-core, and run an audit headlessly.

  1. Create scan.js:
const puppeteer = require('puppeteer');
const axeSource = require('axe-core').source;

(async () => {
  const browser = await puppeteer.launch();
  const page    = await browser.newPage();
  await page.goto('https://your-site.example.com');

  // Inject axe-core into the page context
  await page.evaluate(axeSource);

  // Run axe and retrieve results
  const results = await page.evaluate(async () => {
    return await axe.run(document, {
      runOnly: { type: 'tag', values: ['wcag2aa'] }
    });
  });

  if (results.violations.length) {
    console.error('Accessibility issues:', results.violations);
    process.exit(1);
  } else {
    console.log('No accessibility violations found');
  }

  await browser.close();
})();
  1. Run the script:
node scan.js

4. Continuous Integration

In CI, install dependencies, run your scan script, and fail the build on violations.

Example: GitHub Actions

Create .github/workflows/accessibility.yml:

name: Accessibility Scan

on: [push, pull_request]

jobs:
  axe-scan:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2

      - name: Set up Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '16'

      - name: Install dependencies
        run: npm ci

      - name: Run accessibility scan
        run: node scan.js

Example: npm Script

Add to your package.json:

"scripts": {
  "accessibility": "node scan.js"
}

Then in any CI job:

npm run accessibility

Next Steps

  • Scope audits to specific selectors (axe.run('#main', {...})) to speed up checks.
  • Customize rules in options (e.g. rules: { 'color-contrast': { enabled: false } }).
  • Integrate reports into dashboards by supplying a custom reporter function.

Core Concepts

Axe-core models accessibility rules as JavaScript objects that pair DOM‐level selectors with test functions (“checks”), runs them on a flattened virtual DOM, and classifies outcomes using shared constants. Understanding this model lets you read, extend, or optimize axe-core rules and checks.

Rule Definition

A rule object drives element selection and check execution:

// Example: lib/rules/image-alt.js
export const imageAlt = {
  id: 'image-alt',
  selector: 'img',                       // CSS selector for candidates
  matches: (node) => !node.hasAttribute('role'), 
  any: ['has-alt-text'],                  // pass if any of these checks pass
  all: [],                                // run all checks must pass
  none: ['is-hidden'],                    // fail if any of these checks pass
  metadata: {
    description: 'Images must have alternate text',
    help: 'Provide meaningful alt text for images'
  },
  tags: ['wcag2a', 'best-practice']
};

Properties:

  • selector (string): Query candidate elements.
  • matches(node, virtualNode?) (fn): Further filter matches.
  • all, any, none (string[]): Arrays of check IDs.
  • metadata/tags: Describe WCAG mapping and grouping.

Lifecycle:

  1. Collect: document.querySelectorAll(selector)
  2. Filter: call matches (if defined), skip hidden by default.
  3. Flatten: build a VirtualNode tree (see “Virtual DOM”).
  4. Execute: run checks named in all/any/none.
  5. Report: combine check statuses into rule result.

Check Definition

Checks encapsulate logic returning a standardized result:

// Example: lib/checks/has-alt-text.js
import { Result, results } from 'axe-core/lib/core/constants';

export function hasAltText({ node }) {
  const alt = node.getAttribute('alt') || '';
  return alt.trim()
    ? { status: results.PASS }
    : { status: results.FAIL, message: 'Missing alt text' };
}

Key points:

  • Receives a context object { node, virtualNode, options }.
  • Returns a CheckResult: { status, message?, data? }.
  • Refer to constants for status values: PASS, FAIL, WARNING, INCOMPLETE.

Virtual DOM

Axe-core flattens real DOM (including Shadow DOM) into VirtualNode instances:

interface VirtualNode {
  el: Node;                         // original DOM node
  parent: VirtualNode | null;
  children: VirtualNode[];
  shadowRootId?: string;            // when entering Shadow DOM
  // root node also has:
  _selectorMap: Map<Node, string[]>; 
  _hasShadowRoot: boolean;
}

Use getFlattenedTree to build it:

import getFlattenedTree from 'axe-core/lib/core/utils/get-flattened-tree';

const vnodes = getFlattenedTree(document.body);
console.log(vnodes[0]._hasShadowRoot);

Benefits:

  • Uniform traversal across light and shadow DOM.
  • Cached selector lookups in _selectorMap speed repeated rule evaluations.

Constants

Shared values ensure consistency across rules, checks, and reporting:

import {
  results,          // { PASS, FAIL, WARNING, INCOMPLETE }
  impactValues,     // ['critical','serious','moderate','minor']
  statusDisplay,    // maps statuses to UI colors/icons
  url }             // URL builder for help links
  from 'axe-core/lib/core/constants';

console.log(results.PASS);           // "passed"
console.log(impactValues);           // ['critical','serious','moderate','minor']

Typical uses:

  • Assign metadata.impact = impactValues[1] for “serious”.
  • Format result URLs: url('image-alt').

Performance & Extension Tips

  • Scope selectors narrowly to limit DOM queries.
  • Reuse the root’s _selectorMap for custom lookups.
  • Cache flattened trees when testing static content.
  • Place shared filter or check helpers under lib/matches/ or lib/checks/.
  • Leverage constants for status and impact to stay aligned with built-in rules.

Usage & Integration Guide

Everything a product-team engineer needs to customise axe-core for their stack: configure checks, target specific DOM contexts, and ensure compatibility across releases.

Customizing Accessibility Checks

Override default check options at runtime via axe.configure. Pass a checks array with { id, options } entries matching each check’s metadata.

Key Points

  • id: string identifier of the check (e.g. "color-contrast")
  • options: shape matches the check’s JSON metadata under lib/checks/...
  • Invalid id or malformed options throws a TypeError
  • Subsequent axe.run calls inherit your configuration

Examples

  1. Adjust color-contrast thresholds for large text only:
import axe from 'axe-core';

axe.configure({
  checks: [
    {
      id: 'color-contrast',
      options: {
        contrastRatio: {
          normal: { expected: 4.5 },
          large:  { expected: 3.0 }  // warn large text if < 3.0
        }
      }
    }
  ]
});

await axe.run(document);
  1. Permit additional language attributes in the has-lang check:
axe.configure({
  checks: [
    {
      id: 'has-lang',
      options: {
        attributes: ['lang', 'xml:lang', 'hreflang']
      }
    }
  ]
});
  1. Combine multiple check overrides in one call:
axe.configure({
  checks: [
    { id: 'color-contrast', options: { contrastRatio: { normal: { expected: 5.0 }, large: { expected: 3.0 } } } },
    { id: 'role-allowed-attr', options: { // custom role allowances
        roleName: ['button', 'link'],
        attributes: ['aria-pressed', 'aria-expanded']
      }
    }
  ],
  rules: [
    { id: 'image-alt', enabled: false }  // disable the image-alt rule
  ]
});

Targeting Specific Elements

Use the context parameter in axe.run to include/exclude nodes, shadow DOM hosts, or iframes.

1. Include & Exclude Selectors

// Only audit main landmarks, skip all buttons
await axe.run({
  include: ['main', '[role="region"]'],
  exclude: ['button']
});

2. Limiting Shadow DOM Testing

// Test <my-widget>’s internal form only
await axe.run({
  fromShadowDom: ['my-widget', 'form.search']
});

// Nested shadow roots: root → header → search input
await axe.run({
  fromShadowDom: ['app-root', '.header', '#search-input']
});

3. Combining Include/Exclude with Shadow DOM

await axe.run({
  include: {
    fromShadowDom: ['#shell', 'app-footer']
  },
  exclude: {
    fromShadowDom: ['#shell', 'app-footer', '.ad-banner']
  }
});

4. Testing Specific Frames

// Audit only the iframe at #help-frame
await axe.run({
  fromFrames: ['#help-frame']
});

Practical Tips

  • Attach custom elements to document before testing.
  • Slotted light-DOM children auto-include when their host is targeted.
  • Don’t mix fromShadowDom directly in include/exclude arrays—wrap as shown.
  • For frame + shadow combos, use nested objects:
    { fromFrames: ['#f'], fromShadowDom: ['host', 'target'] }

Maintaining Backwards Compatibility

axe-core guarantees stability of its public API (functions, JSON structures, and utils) across patch and minor releases. Follow these practices:

  • Use only public APIs: axe.run, axe.configure, axe.reset, axe.version, axe.getRules, etc.
  • Avoid internal utils under axe.utils and direct edits of lib/checks/*/*.json.
  • Pin minor versions ("axe-core": "^4.5.0") to prevent unexpected breaking changes.
  • Test custom rules after each minor upgrade; leverage CI to run axe.run against key pages/components.
  • Track deprecations in release notes. When an API pending removal appears, migrate before the next major.

Example: Guarding on axe.version

import axe from 'axe-core';

const [major] = axe.version.split('.');
if (parseInt(major, 10) < 4) {
  console.warn('Upgrade to axe-core v4+ to use custom configurations');
}

// Safe to call configure and run
axe.configure({ /* ... */ });
await axe.run(document);

Following these guidelines ensures your custom rules and configurations continue working smoothly across axe-core releases.

API Reference

Cleanup API

Restore the page to its pre-audit state by running each plugin’s cleanup routine (including within iframes), then resolve or reject with any errors.

When to use
After you finish an axe.run() and any custom plugin logic that modified the DOM or injected scripts/styles. Calling axe.cleanup() ensures all plugins and frames revert their changes.

Key behaviors

  • Invokes plugin.cleanup(resolve, reject) for each registered plugin
  • Sends { command: 'cleanup-plugin' } to all <iframe>/<frame> nodes
  • Collects any cleanup errors and rejects with an array of errors
  • Throws an Error if no audit is configured (axe._audit)

Signature

axe.cleanup(resolve?: Function, reject?: Function): Promise<any[]>

Parameters

  • resolve (optional): Called with an array of results on success
  • reject (optional): Called with an array of Error instances on failure (defaults to axe.log)

Returns
A Promise that:

  • resolves to an array of plugin/frame cleanup results
  • rejects with an array of Error instances

Example usage

// Promise style
axe.run(document, { /* options */ })
  .then(results => {
    console.log('Audit results:', results);
    return axe.cleanup();
  })
  .then(cleanupResults => {
    console.log('All plugins cleaned up:', cleanupResults);
  })
  .catch(errs => {
    console.error('Cleanup errors:', errs);
  });

// Callback style
function onCleanupDone(results) {
  console.log('Cleanup finished', results);
}
function onCleanupError(errors) {
  console.warn('Some plugins failed to clean up', errors);
}
axe.cleanup(onCleanupDone, onCleanupError);

Practical tips

  • Always call axe.cleanup() after modifying the DOM via plugins to prevent memory leaks.
  • For custom plugins, implement:
    axe.plugins.myPlugin = {
      cleanup(resolve, reject) {
        try {
          document.body.classList.remove('myInjectedClass');
          resolve('myPlugin cleaned');
        } catch (e) {
          reject(e);
        }
      }
    };
    
  • To clean up within frames, listen for 'message' events in the frame and teardown on receiving { command: 'cleanup-plugin' }.
  • Handle multiple errors via Array.isArray(errors).

DqElement: Element Serialization and Selectors

Wrap an HTMLElement (or virtual node) to capture CSS selectors, ancestry, XPath, and serialized source for accessibility reporting. Configure how elements appear in results and extract human-readable identifiers.

Instantiation

new DqElement(element, options?, spec?)
  • options defaults to settings from DqElement.setRunOptions().
  • spec is used when merging frame contexts (fromFrame).

Core getters

  • dq.selector – array of one or more unique, readable CSS selectors
  • dq.ancestry – array of ancestor selectors up to the root
  • dq.xpath – array of XPath strings
  • dq.element – original HTMLElement reference (if enabled)
  • dq.source – truncated outerHTML (unless axe._audit.noHtml is true)

JSON serialization

JSON.stringify(dq) invokes dq.toJSON() and outputs:

  • selector, ancestry, xpath, source
  • nodeIndexes (flat-tree sort keys)
  • fromFrame boolean
  • element if elementRef was enabled

Frame support

DqElement.fromFrame(childSpec, options?, parentFrameSpec)

Merges child and parent specs so selectors/xpaths reflect nested iframe contexts.

Global run-options

DqElement.setRunOptions({ elementRef: Boolean, absolutePaths: Boolean });
  • elementRef: include raw DOM node in JSON
  • absolutePaths: generate selectors from document root

Practical Usage

  1. Configure default options:

    import DqElement from 'axe-core/lib/core/utils/dq-element';
    DqElement.setRunOptions({ elementRef: true, absolutePaths: true });
    
  2. Wrap an element:

    const dq = new DqElement(document.querySelector('button'));
    console.log(dq.selector[0]);      // "#nav > .menu-item[data-active]"
    console.log(dq.ancestry);
    console.log(dq.xpath[0]);         // "/html/body/div[2]/nav/ul/li[3]/button"
    console.log(dq.source);
    console.log(dq.element);          // HTMLElement (if enabled)
    
  3. Serialize for a JSON report:

    const reportItem = { id: 'rule-1', nodes: [dq] };
    const json = JSON.stringify(reportItem, null, 2);
    console.log(json);
    
  4. Handling iframes:

    // childSpec from axe’s child frame result
    // parentFrameSpec from parent DqElement.toJSON()
    const merged = DqElement.fromFrame(childSpec, null, parentFrame.toJSON());
    console.log(merged.selector);
    // [ "iframe#checkout", ".item-button" ]
    

axe.run: Running Accessibility Analysis

Invoke axe-core to evaluate a page or sub-tree for accessibility issues. Supports Promise and callback styles, scoped scans, and customizable rule sets.

Signatures

// Promise-based
run<T = AxeResults>(context?: ElementContext): Promise<T>
run<T = AxeResults>(options: RunOptions): Promise<T>
run<T = AxeResults>(context: ElementContext, options: RunOptions): Promise<T>

// Callback-based
run<T = AxeResults>(callback: RunCallback<T>): void
run<T = AxeResults>(context: ElementContext, callback: RunCallback<T>): void
run<T = AxeResults>(options: RunOptions, callback: RunCallback<T>): void
run<T = AxeResults>(context: ElementContext, options: RunOptions, callback: RunCallback<T>): void

Key Types

  • ElementContext: CSS selector string | DOM Node | NodeList | array of selectors | { include: ContextProp; exclude?: ContextProp }
  • RunOptions (partial):
    • runOnly: specify rules or tags to run
    • rules: enable/disable specific rules
    • resultTypes: ['violations', 'passes', …]
    • selectors, xpath, ancestry: include metadata on failures
    • iframes: include iframes in scan
    • timing: frameWaitTime, pingWaitTime
    • preload: { assets: string[]; timeout?: number }

Practical Examples

  1. Full-page scan (Promise)

    import axe from 'axe-core';
    
    const results = await axe.run();
    console.log('Violations:', results.violations);
    
  2. Scan a region with include/exclude

    const { violations, incomplete } = await axe.run(
      { include: '#main-content', exclude: ['.modal', '.tooltip'] },
      { resultTypes: ['violations', 'incomplete'], selectors: true }
    );
    
  3. Limit rules (callback)

    axe.run(
      document.body,
      {
        runOnly: { type: 'tag', values: ['wcag2a', 'wcag2aa'] },
        rules: { 'color-contrast': { enabled: false } }
      },
      (err, results) => {
        if (err) throw err;
        console.table(results.passes, ['id', 'impact', 'help']);
      }
    );
    
  4. Scanning Shadow DOM

    const shadow = document.querySelector('#widget')!.shadowRoot!;
    const { violations } = await axe.run({ include: shadow.querySelectorAll('button') });
    console.log(violations);
    

Best Practices

  • Prefer Promise style for async/await flows.
  • Limit scope with include/exclude for performance.
  • Combine runOnly and rules to target specific WCAG levels or components.
  • Enable selectors, xpath, or ancestry for integration with reporting tools.
  • For multi-frame pages, set iframes: true and adjust frameWaitTime if needed.

Advanced Topics

Deep-dive material for power users and maintainers of axe-core.

Performance Timer Utility

Shim the User Timing API to measure and log durations of axe-core audits and phases, with a graceful fallback.

Import

import performanceTimer from 'axe-core/lib/core/utils/performance-timer';

API Methods

  • start()
    Marks mark_axe_start.
  • end()
    Marks mark_axe_end, measures axe from start to end, then logs.
  • auditStart()
    Marks mark_audit_start.
  • auditEnd()
    Marks mark_audit_end, measures audit_start_to_end, then logs all measures.
  • mark(name: string)
    Creates or overwrites a timestamp.
  • measure(name: string, startMark: string, endMark: string)
    Defines a measurement between two marks.
  • logMeasures(name?: string)
    Logs all measures since mark_axe_start or only those matching name.
  • timeElapsed(): number
    Returns ms since last reset() or module load.
  • reset()
    Resets internal baseline for timeElapsed().

Examples

Basic full-audit timing:

import performanceTimer from 'axe-core/lib/core/utils/performance-timer';
import axe from 'axe-core';

performanceTimer.start();
axe.run(document, {}, results => {
  performanceTimer.end();
  // Console: Measure axe took 123.45ms
  console.log('Accessibility results:', results);
});

Per-frame audit timing:

performanceTimer.auditStart();
axe.run(frameDocument, {}, frameResults => {
  performanceTimer.auditEnd();
  // Console includes 'audit_start_to_end'
});

Custom phase timing:

performanceTimer.mark('phase1-begin');
// ...phase 1 logic...
performanceTimer.mark('phase1-end');
performanceTimer.measure('phase1', 'phase1-begin', 'phase1-end');
performanceTimer.logMeasures('phase1'); // logs only 'phase1'

One-off interval and reset:

const sinceLast = performanceTimer.timeElapsed();
console.log(`Time since last audit: ${sinceLast}ms`);
performanceTimer.reset();

Practical Guidance

  • Ensure your environment supports the User Timing API; otherwise calls are no-ops.
  • Wrap axe.run with start()/end() to capture full audit duration.
  • Use auditStart()/auditEnd() or custom marks for segmented audits.
  • Logs use axe’s log module (usually console.log), so you can hook or filter them.
  • Call reset() before a new session to keep timeElapsed() meaningful.

Creating and Updating Translation Files

Guide for generating new locale JSON files and synchronizing translations with the English source.

1. Generate or Refresh

Run the Grunt translate task with your language code:

grunt translate --lang=fr

This creates or overwrites locales/fr.json from _template.json.

2. JSON Structure

{
  "lang": "fr",
  "rules": {
    "accesskeys": {
      "description": "S’assurer que chaque valeur d’attribut accesskey est unique",
      "help": "La valeur de l’attribut accesskey doit être unique"
    }
  },
  "checks": {
    "accesskeys": {
      "pass": "Valeur d’accesskey unique",
      "fail": "Le document contient plusieurs éléments avec la même accesskey"
    }
  }
}
  • lang: language code
  • rules: maps rule IDs to description and help
  • checks: maps check IDs to pass/fail/(optional) incomplete, with singular/plural variants

3. Translate

  • Replace English text in all message fields.
  • Preserve interpolation tokens (e.g. ${data.values}).
  • Do not alter keys or nesting.

4. Keep Up to Date

Whenever English source changes:

grunt translate --lang=fr
  • Merge new keys and translate them.
  • Remove or flag obsolete entries.

5. Preview and Verify

  • Run axe-core tests or integrate the locale in your build.
  • Inspect reports to confirm correct rendering.

Reporting Security Issues

Approved process for reporting vulnerabilities in Deque-owned repositories. Do not open public issues; email reports to security@deque.com.

Report Contents

  • Type of issue (e.g. XSS, SQL injection)
  • Affected files (full paths)
  • Location (branch/tag/commit hash or URL)
  • Environment (OS, browser, library versions)
  • Reproduction steps (detailed)
  • Proof-of-concept or exploit code
  • Impact assessment

Submission

Email to security@deque.com with subject:

[SECURITY] <Repository Name> – <Short Issue Description>

Email Template

To: security@deque.com
Subject: [SECURITY] axe-core – Cross-Site Scripting in CLI Reporter

Hello Deque Security Team,

1. Type of issue:
   Cross-Site Scripting (CWE-79)

2. Affected files:
   /packages/axe-core/cli/reporter.js

3. Location:
   Tag v4.7.2, commit 9f3d2a1
   https://github.com/dequelabs/axe-core/blob/v4.7.2/packages/cli/reporter.js#L120

4. Environment:
   • Node.js v14.18.0
   • Windows 10 Pro

5. Steps to reproduce:
   a. Install axe-core CLI globally.
   b. Run `axe https://example.com --reporter html --output report.html`.
   c. Include `<script>alert('XSS')</script>` in data attributes.
   d. Open report.html—alert appears.

6. Proof-of-concept:
   Attached minimal HTML file `xss-demo.html`.

7. Impact:
   Crafted HTML can execute arbitrary JS when viewing the report.

Please let me know if you need additional details.

Thank you,
Jane Developer

Tips

  • Use [SECURITY] prefix to expedite triage.
  • Attach minimal repro code or logs.
  • Provide clear, concise steps.
  • Communicate in English for faster response.

Development Guide

This guide covers repository layout, coding standards, local development, testing, and the release process for axe-core contributors and maintainers.

Repository Layout

/                ← Project root
├─ .editorconfig  ← Coding style rules
├─ CONTRIBUTING.md
├─ Gruntfile.js   ← Build, test, and locale tasks
├─ package.json
├─ src/           ← Core modules and rules
├─ test/          ← Unit and integration tests
├─ locales/       ← Translation JSON files
└─ doc/
   └─ code-submission-guidelines.md

Coding Standards

.editorconfig Snippet

Ensures consistent formatting across editors:

root = true

[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true

[*.{json,yml,yaml,md}]
trim_trailing_whitespace = false

Commit Message Format

Follow CONTRIBUTING.md and code-submission-guidelines.md:

<type>(<scope>): <short description>

<body explaining motivation and changes>

• Types: feat, fix, chore, docs
• Use imperative mood and reference issue numbers when relevant.

Code Structure & Documentation

  • Return-early pattern:

    function isValid(node) {
      if (!node) return false;
      // main logic
    }
    export default isValid;
    
  • Default export at file bottom

  • DocBlock comments for public functions:

    /**
     * Evaluates whether `node` meets the foo rule.
     * @param {Element} node – DOM node to inspect.
     * @returns {boolean}
     */
    function checkFoo(node) { … }
    
    export default checkFoo;
    

Development Setup & Build

  1. Clone and install dependencies:

    git clone https://github.com/dequelabs/axe-core.git
    cd axe-core
    npm ci
    npm run fmt       # Formats code via Prettier
    
  2. Common Grunt tasks:

    • Build core bundles:

      grunt build
      
    • Run all tests:

      grunt test
      
    • Generate a translation template:

      grunt add-locale:template --lang xyz
      
    • Scaffold a new locale (e.g. French):

      grunt add-locale:newLang --lang fr
      
    • Build localized bundles:

      grunt build --lang fr
      

Testing

Write comprehensive tests—including Shadow DOM scenarios—and use the built-in fixtures utilities.

  • Run tests:

    npm test
    npm test:locales   # Validates locale JSON schema
    
  • Fixture utilities (axe.testUtils):

    import {
      getFixture,
      injectIntoFixture,
      fixtureSetup,
      queryFixture,
      checkSetup
    } from 'axe-core/testUtils';
    
    // Inject HTML and run axe.setup
    const root = fixtureSetup(`
      <button id="target" aria-label="Click me"></button>
    `);
    
    // Prepare and invoke a single check
    const [node, opts, vNode] = checkSetup(
      '<button id="target" aria-label="Click"></button>',
      { someOption: true },
      '#target'
    );
    const result = checks['aria-button-name'].evaluate.call(
      new MockCheckContext(), node, opts, vNode, {}
    );
    

Ensure each new rule or feature includes unit tests in test/, uses DocBlock comments, and follows the return-early pattern.

Release Process

  1. Bump version in package.json and update CHANGELOG.md.

  2. Run a clean build:

    npm ci
    grunt build
    
  3. Commit and tag:

    git add package.json CHANGELOG.md
    git commit -m "chore(release): vX.Y.Z"
    git tag vX.Y.Z
    git push origin main --tags
    
  4. Publish to npm:

    npm publish
    
\n```\n\n### 2. Browser Quick Start\n\nRun an audit on page load and log violations:\n```html\n\n\n \n \n \n \n \n \n \n\n```\n\n### 3. Node.js Quick Start with Puppeteer\n\nUse Puppeteer to load a page, inject axe-core, and run an audit headlessly.\n\n1. Create `scan.js`:\n```js\nconst puppeteer = require('puppeteer');\nconst axeSource = require('axe-core').source;\n\n(async () => {\n const browser = await puppeteer.launch();\n const page = await browser.newPage();\n await page.goto('https://your-site.example.com');\n\n // Inject axe-core into the page context\n await page.evaluate(axeSource);\n\n // Run axe and retrieve results\n const results = await page.evaluate(async () => {\n return await axe.run(document, {\n runOnly: { type: 'tag', values: ['wcag2aa'] }\n });\n });\n\n if (results.violations.length) {\n console.error('Accessibility issues:', results.violations);\n process.exit(1);\n } else {\n console.log('No accessibility violations found');\n }\n\n await browser.close();\n})();\n```\n\n2. Run the script:\n```bash\nnode scan.js\n```\n\n### 4. Continuous Integration\n\nIn CI, install dependencies, run your scan script, and fail the build on violations.\n\n#### Example: GitHub Actions\nCreate `.github/workflows/accessibility.yml`:\n```yaml\nname: Accessibility Scan\n\non: [push, pull_request]\n\njobs:\n axe-scan:\n runs-on: ubuntu-latest\n\n steps:\n - uses: actions/checkout@v2\n\n - name: Set up Node.js\n uses: actions/setup-node@v3\n with:\n node-version: '16'\n\n - name: Install dependencies\n run: npm ci\n\n - name: Run accessibility scan\n run: node scan.js\n```\n\n#### Example: npm Script\nAdd to your `package.json`:\n```json\n\"scripts\": {\n \"accessibility\": \"node scan.js\"\n}\n```\nThen in any CI job:\n```bash\nnpm run accessibility\n```\n\n## Next Steps\n\n- Scope audits to specific selectors (`axe.run('#main', {...})`) to speed up checks.\n- Customize rules in options (e.g. `rules: { 'color-contrast': { enabled: false } }`).\n- Integrate reports into dashboards by supplying a custom `reporter` function.\n## Core Concepts\n\nAxe-core models accessibility rules as JavaScript objects that pair DOM‐level selectors with test functions (“checks”), runs them on a flattened virtual DOM, and classifies outcomes using shared constants. Understanding this model lets you read, extend, or optimize axe-core rules and checks.\n\n### Rule Definition\n\nA rule object drives element selection and check execution:\n\n```js\n// Example: lib/rules/image-alt.js\nexport const imageAlt = {\n id: 'image-alt',\n selector: 'img', // CSS selector for candidates\n matches: (node) => !node.hasAttribute('role'), \n any: ['has-alt-text'], // pass if any of these checks pass\n all: [], // run all checks must pass\n none: ['is-hidden'], // fail if any of these checks pass\n metadata: {\n description: 'Images must have alternate text',\n help: 'Provide meaningful alt text for images'\n },\n tags: ['wcag2a', 'best-practice']\n};\n```\n\nProperties:\n- `selector` (string): Query candidate elements.\n- `matches(node, virtualNode?)` (fn): Further filter matches.\n- `all`, `any`, `none` (string[]): Arrays of check IDs.\n- `metadata`/`tags`: Describe WCAG mapping and grouping.\n\nLifecycle:\n1. **Collect**: `document.querySelectorAll(selector)`\n2. **Filter**: call `matches` (if defined), skip hidden by default.\n3. **Flatten**: build a `VirtualNode` tree (see “Virtual DOM”).\n4. **Execute**: run checks named in `all`/`any`/`none`.\n5. **Report**: combine check statuses into rule result.\n\n### Check Definition\n\nChecks encapsulate logic returning a standardized result:\n\n```js\n// Example: lib/checks/has-alt-text.js\nimport { Result, results } from 'axe-core/lib/core/constants';\n\nexport function hasAltText({ node }) {\n const alt = node.getAttribute('alt') || '';\n return alt.trim()\n ? { status: results.PASS }\n : { status: results.FAIL, message: 'Missing alt text' };\n}\n```\n\nKey points:\n- Receives a context object `{ node, virtualNode, options }`.\n- Returns a `CheckResult`: `{ status, message?, data? }`.\n- Refer to constants for `status` values: `PASS`, `FAIL`, `WARNING`, `INCOMPLETE`.\n\n### Virtual DOM\n\nAxe-core flattens real DOM (including Shadow DOM) into `VirtualNode` instances:\n\n```ts\ninterface VirtualNode {\n el: Node; // original DOM node\n parent: VirtualNode | null;\n children: VirtualNode[];\n shadowRootId?: string; // when entering Shadow DOM\n // root node also has:\n _selectorMap: Map; \n _hasShadowRoot: boolean;\n}\n```\n\nUse `getFlattenedTree` to build it:\n\n```js\nimport getFlattenedTree from 'axe-core/lib/core/utils/get-flattened-tree';\n\nconst vnodes = getFlattenedTree(document.body);\nconsole.log(vnodes[0]._hasShadowRoot);\n```\n\nBenefits:\n- Uniform traversal across light and shadow DOM.\n- Cached selector lookups in `_selectorMap` speed repeated rule evaluations.\n\n### Constants\n\nShared values ensure consistency across rules, checks, and reporting:\n\n```js\nimport {\n results, // { PASS, FAIL, WARNING, INCOMPLETE }\n impactValues, // ['critical','serious','moderate','minor']\n statusDisplay, // maps statuses to UI colors/icons\n url } // URL builder for help links\n from 'axe-core/lib/core/constants';\n\nconsole.log(results.PASS); // \"passed\"\nconsole.log(impactValues); // ['critical','serious','moderate','minor']\n```\n\nTypical uses:\n- Assign `metadata.impact = impactValues[1]` for “serious”.\n- Format result URLs: `url('image-alt')`.\n\n### Performance & Extension Tips\n\n- Scope selectors narrowly to limit DOM queries.\n- Reuse the root’s `_selectorMap` for custom lookups.\n- Cache flattened trees when testing static content.\n- Place shared filter or check helpers under `lib/matches/` or `lib/checks/`.\n- Leverage constants for status and impact to stay aligned with built-in rules.\n## Usage & Integration Guide\n\nEverything a product-team engineer needs to customise axe-core for their stack: configure checks, target specific DOM contexts, and ensure compatibility across releases.\n\n### Customizing Accessibility Checks\n\nOverride default check options at runtime via `axe.configure`. Pass a `checks` array with `{ id, options }` entries matching each check’s metadata.\n\n#### Key Points\n- `id`: string identifier of the check (e.g. `\"color-contrast\"`)\n- `options`: shape matches the check’s JSON metadata under `lib/checks/...`\n- Invalid `id` or malformed `options` throws a `TypeError`\n- Subsequent `axe.run` calls inherit your configuration\n\n#### Examples\n\n1. Adjust color-contrast thresholds for large text only:\n```js\nimport axe from 'axe-core';\n\naxe.configure({\n checks: [\n {\n id: 'color-contrast',\n options: {\n contrastRatio: {\n normal: { expected: 4.5 },\n large: { expected: 3.0 } // warn large text if < 3.0\n }\n }\n }\n ]\n});\n\nawait axe.run(document);\n```\n\n2. Permit additional language attributes in the `has-lang` check:\n```js\naxe.configure({\n checks: [\n {\n id: 'has-lang',\n options: {\n attributes: ['lang', 'xml:lang', 'hreflang']\n }\n }\n ]\n});\n```\n\n3. Combine multiple check overrides in one call:\n```js\naxe.configure({\n checks: [\n { id: 'color-contrast', options: { contrastRatio: { normal: { expected: 5.0 }, large: { expected: 3.0 } } } },\n { id: 'role-allowed-attr', options: { // custom role allowances\n roleName: ['button', 'link'],\n attributes: ['aria-pressed', 'aria-expanded']\n }\n }\n ],\n rules: [\n { id: 'image-alt', enabled: false } // disable the image-alt rule\n ]\n});\n```\n\n### Targeting Specific Elements\n\nUse the `context` parameter in `axe.run` to include/exclude nodes, shadow DOM hosts, or iframes.\n\n#### 1. Include & Exclude Selectors\n```js\n// Only audit main landmarks, skip all buttons\nawait axe.run({\n include: ['main', '[role=\"region\"]'],\n exclude: ['button']\n});\n```\n\n#### 2. Limiting Shadow DOM Testing\n```js\n// Test ’s internal form only\nawait axe.run({\n fromShadowDom: ['my-widget', 'form.search']\n});\n\n// Nested shadow roots: root → header → search input\nawait axe.run({\n fromShadowDom: ['app-root', '.header', '#search-input']\n});\n```\n\n#### 3. Combining Include/Exclude with Shadow DOM\n```js\nawait axe.run({\n include: {\n fromShadowDom: ['#shell', 'app-footer']\n },\n exclude: {\n fromShadowDom: ['#shell', 'app-footer', '.ad-banner']\n }\n});\n```\n\n#### 4. Testing Specific Frames\n```js\n// Audit only the iframe at #help-frame\nawait axe.run({\n fromFrames: ['#help-frame']\n});\n```\n\n#### Practical Tips\n- Attach custom elements to `document` before testing.\n- Slotted light-DOM children auto-include when their host is targeted.\n- Don’t mix `fromShadowDom` directly in `include`/`exclude` arrays—wrap as shown.\n- For frame + shadow combos, use nested objects: \n `{ fromFrames: ['#f'], fromShadowDom: ['host', 'target'] }`\n\n### Maintaining Backwards Compatibility\n\naxe-core guarantees stability of its public API (functions, JSON structures, and utils) across patch and minor releases. Follow these practices:\n\n- **Use only public APIs**: `axe.run`, `axe.configure`, `axe.reset`, `axe.version`, `axe.getRules`, etc.\n- **Avoid internal utils** under `axe.utils` and direct edits of `lib/checks/*/*.json`.\n- **Pin minor versions** (`\"axe-core\": \"^4.5.0\"`) to prevent unexpected breaking changes.\n- **Test custom rules** after each minor upgrade; leverage CI to run `axe.run` against key pages/components.\n- **Track deprecations** in release notes. When an API pending removal appears, migrate before the next major.\n\n#### Example: Guarding on `axe.version`\n```js\nimport axe from 'axe-core';\n\nconst [major] = axe.version.split('.');\nif (parseInt(major, 10) < 4) {\n console.warn('Upgrade to axe-core v4+ to use custom configurations');\n}\n\n// Safe to call configure and run\naxe.configure({ /* ... */ });\nawait axe.run(document);\n```\n\nFollowing these guidelines ensures your custom rules and configurations continue working smoothly across axe-core releases.\n## API Reference\n\n### Cleanup API\n\nRestore the page to its pre-audit state by running each plugin’s cleanup routine (including within iframes), then resolve or reject with any errors.\n\nWhen to use \nAfter you finish an `axe.run()` and any custom plugin logic that modified the DOM or injected scripts/styles. Calling `axe.cleanup()` ensures all plugins and frames revert their changes.\n\nKey behaviors \n- Invokes `plugin.cleanup(resolve, reject)` for each registered plugin \n- Sends `{ command: 'cleanup-plugin' }` to all `