Auditing an Application

Auditing works in three layers: a linter on your JSX, an engine on the rendered accessibility tree, and a human with a keyboard. Each catches what the others cannot. Take a signup card with four defects — a bare <input>, an icon-only <button> holding nothing but an SVG, #a9a9a9 helper text on white, and on line 7 a <div onClick> as the submit button.

The linter runs first because it works on source. Install eslint-plugin-jsx-a11y (6.10.2) and spread jsxA11y.flatConfigs.recommended.rules into eslint.config.js. ESLint 9 39,810 reports the fake button, twice (messages wrapped to fit):

Output of 118
src\Signup.jsx
  7:7  error  Visible, non-interactive elements with click handlers must have
        at least one keyboard listener  jsx-a11y/click-events-have-key-events
  7:7  error  Avoid non-native interactive elements. If using native HTML is not
        possible, add an appropriate role...  jsx-a11y/no-static-element-interactions

Layer two is axe-core 7,562 , Deque's rules engine (github.com/dequelabs/axe-core (https://github.com/dequelabs/axe-core 7,562 ), 4.13.0, MPL-2.0). It walks a real browser's accessibility tree, so it sees computed color, computed names and ARIA that exists only after a render. @axe-core/playwright wires it into a page; wait for a committed node or you audit an empty root.

Running axe-core against a rendered React pageJavaScript
import { chromium } from 'playwright';
import AxeBuilder from '@axe-core/playwright';
const browser = await chromium.launch();
const page = await (await browser.newContext()).newPage();
await page.goto(process.argv[2]);
await page.waitForSelector('button');
const { violations } = await new AxeBuilder({ page }).analyze();
console.log(`${violations.length} violations`);
for (const v of violations)
  console.log(`${v.impact}\t${v.id}\t${v.nodes[0].target}\t${v.help}`);

Run that against the card, then against a copy with <label htmlFor>, aria-label="Dismiss" and aria-hidden="true" on the icon button, and #595959 helper text:

Output of 119
$ node a11y.js before.html
3 violations
critical  button-name  button  Buttons must have discernible text
serious  color-contrast  p  Elements must meet minimum color contrast thresholds
critical  label  input  Form elements must have labels
$ node a11y.js after.html
0 violations

Compare the reports. axe found the three defects the linter missed and said nothing about the <div onClick>: with no role and no name it is indistinguishable from decoration. The linter found only that one. Zero violations means no rule fired — axe-core's README claims 57% of WCAG issues on average, and no engine judges whether aria-label="Click here" is useful.

So run all three: @axe-core/react for findings in the dev console, vitest-axe or jest-axe to fail the build on a regression (Testing React Applications), then unplug the mouse and try a screen reader — NVDA 29,469 , VoiceOver or JAWS 51,382 . The DevTools accessibility pane shows the same tree these printouts came from.