How to Configure axe-core for React and Vue Applications

Automated accessibility testing in modern CI/CD pipelines fails when scanner execution outpaces framework rendering. Resolving framework-specific DOM hydration delays and virtual DOM diffing artifacts requires targeted axe-core Configuration & Setup parameters. This blueprint addresses scanner initialization timing, dynamic component boundary isolation, and rule overrides for React and Vue ecosystems.

Implement the following baseline controls to eliminate false positives:

  • Isolate dynamic hydration states before axe-core injection.
  • Configure runOnly and rules to suppress framework-generated noise.
  • Use explicit DOM readiness checks rather than fixed delays.
  • Map component lifecycle hooks to scanner execution timing.

For multi-package setups that share one scanner config across apps, Setting Up axe-core in a Next.js Monorepo extends these same timing guards across workspaces.

Scan timing across the hydration lifecycle An early axe.run during render or StrictMode double-render captures a partial tree and emits phantom violations; waiting for the committed DOM yields a clean, deterministic scan. framework lifecycle (left = earlier) render / hydrate StrictMode 2nd pass committed DOM / idle early axe.run phantom violations scoped axe.run deterministic result
Scanning during render or the StrictMode double-pass reads a partial tree; waiting for the committed DOM (network idle plus a stable container) is what removes the phantom violations.

Root Cause: Virtual DOM Hydration & Timing Mismatches

axe-core traverses the live DOM synchronously. React and Vue manipulate the DOM asynchronously during hydration and route transitions. Premature execution captures incomplete trees.

React StrictMode intentionally double-renders components in development. This can trigger early axe-core execution before state commits. Vue 3 Teleport and Suspense alter the DOM tree structure before the scanner injects. Framework-generated ARIA attributes frequently conflict with static rule expectations. SPA route transitions leave detached nodes lingering in the accessibility tree.

Aligning scanner execution with framework lifecycle events prevents phantom violations. Proper baseline alignment with Web Accessibility Testing Fundamentals & Tool Selection ensures consistent audit coverage across rendering engines.

Configuration: Framework-Specific Run Parameters

Default axe-core settings scan the entire document.body. This captures transient hydration artifacts. Scope the scanner to stable container nodes and use jest-axe for React component testing.

// test-setup.js — using jest-axe for React component tests
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);

// In your test file:
import { render } from '@testing-library/react';

test('MyComponent has no accessibility violations', async () => {
  const { container } = render(<MyComponent />);
  const results = await axe(container, {
    rules: {
      'color-contrast': { enabled: false }
    },
    runOnly: {
      type: 'tag',
      values: ['wcag2a', 'wcag2aa', 'best-practice']
    }
  });
  expect(results).toHaveNoViolations();
});

For Playwright-based E2E tests that need to wait for hydration before scanning:

// playwright-a11y.ts
import { test } from '@playwright/test';
import { injectAxe, checkA11y } from 'axe-playwright';

test('component accessibility post-hydration', async ({ page }) => {
  await page.goto('/dashboard');
  // Wait for the framework to finish rendering before scanning
  await page.waitForLoadState('networkidle');
  await page.waitForSelector('#app-root', { state: 'visible' });
  await injectAxe(page);
  await checkA11y(page, '#app-root', {
    runOnly: {
      type: 'tag',
      values: ['wcag2a', 'wcag2aa', 'best-practice']
    },
    rules: {
      'color-contrast': { enabled: false }
    }
  });
});

This scopes the scanner to #app-root and waits for network idle, bypassing React and Vue hydration flush cycles. Disabling contrast checks for dynamic themes is appropriate when themes are applied client-side and cannot be accurately computed at scan time.

Override framework-injected ARIA attributes that trigger false positives. Tag managed nodes with custom data attributes and configure rule selectors:

// a11y-overrides.js
axe.configure({
  rules: [
    {
      id: 'aria-allowed-attr',
      selector: '[data-framework-generated="true"]',
      enabled: false
    }
  ]
});

This disables scanning on nodes explicitly tagged as framework-managed. Use this sparingly: overriding rules globally masks real violations. Always scope overrides to the tightest possible CSS selector.

Validation: DOM State Verification & False-Positive Resolution

Scanner output must match the actual rendered accessibility tree. Compare axe-core violations directly with the browser DevTools Accessibility pane.

// validation-runner.js
const results = await axe.run(document.querySelector('#app-root'));

const frameworkQuirks = results.violations.filter(v => v.impact === 'minor');
const blocking = results.violations.filter(v =>
  v.impact === 'critical' || v.impact === 'serious'
);

frameworkQuirks.forEach(v => {
  console.warn(`[Framework Quirk] ${v.id}: ${v.nodes.length} instances suppressed`);
});

if (blocking.length > 0) {
  console.error(`${blocking.length} blocking violations found`);
  process.exit(1);
}

Minor violations are logged but excluded from pipeline failures. Validate all remaining issues against a WCAG 2.2 AA baseline before merging.

Pipeline Impact: CI/CD Integration & Threshold Enforcement

Embed the configured scanner into CI workflows with fail-fast conditions. Set exit code thresholds to differentiate critical blockers from moderate warnings.

// ci-runner.js
const axe = require('axe-core');

const results = await axe.run(document);
const critical = results.violations.filter(v => v.impact === 'critical');
const moderate = results.violations.filter(v => v.impact === 'moderate');

if (critical.length > 0) {
  console.error(`FAIL: ${critical.length} critical violations detected`);
  process.exit(1);
}

// Write JSON report for PR annotation
const fs = require('fs');
fs.writeFileSync(
  'a11y-results.json',
  JSON.stringify(results.violations, null, 2)
);
console.log(`CI PASS: ${moderate.length} moderate issues logged for review`);
# .github/workflows/a11y-check.yml
name: Accessibility Gate
on: [pull_request]
jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run test:a11y
      - name: Upload violation report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: a11y-results
          path: a11y-results.json

Use actions/upload-artifact to preserve the JSON output for developer review, rather than relying on SARIF format which requires a separate conversion step.

Common Pitfalls

  • Running axe-core before React Suspense or Vue Teleport resolves.
  • Ignoring aria-hidden conflicts from framework portal implementations.
  • Overriding color-contrast globally instead of scoping to dynamic components.
  • Failing to call axe.cleanup() between test runs when reusing a browser context, which can cause memory leaks.
  • Using document.body as the target in SPAs with route-based lazy loading.

FAQ

How do I prevent axe-core from flagging React StrictMode double-renders? In unit tests with jest-axe, render the component normally—StrictMode double-renders are internal to React and the final committed DOM is what axe receives. In Playwright E2E tests, use waitForLoadState('networkidle') or waitForSelector targeting a stable element before calling injectAxe.

What is the correct runOnly configuration for Vue 3 component libraries? Set runOnly to { type: 'tag', values: ['wcag2a', 'wcag2aa'] }. Exclude best-practice tags if Vue’s dynamic ARIA generation causes noise; re-enable once the root cause is addressed rather than leaving rules permanently disabled.

How to handle false positives on dynamically injected ARIA attributes? Use axe.configure() with a selector scoped to the specific component. Add data-a11y-ignore attributes to framework-managed nodes only as a last resort, always with a documented justification tied to a tracking ticket.

Can axe-core scan Shadow DOM components in React/Vue? Yes, but you must scope the scan to the shadow host element. Pass the shadow host’s DOM node (not a CSS selector string) as the first argument to axe.run(). Ensure the framework’s CSS encapsulation does not strip computed styles required for contrast and layout checks.