eslint-plugin-aria-a11y · @aria-a11y/cli · v0.2.0 on npm
The accessibility formatter.
It never changes what the code means.
Prettier ended brace-style debates by turning them into a failing build. Aria applies the same discipline to the mechanical slice of accessibility work — the redundant, conflicting, and broken ARIA that pollutes most codebases — so that slice can run on save and gate CI instead of living in review debates.
Everything that would require a guess stays out of the automatic path, by construction.
The core idea
One gate governs the whole system.
Every accessibility fact is classified by where its semantics came from: native (real HTML, per aria-query), declared (explicit author ARIA or design-system config), or inferred (a guess from signals like onClick).
Known semantics
native
The implicit role of real HTML, per aria-query. <button> is a button. Nobody guessed.
declared
Explicit author ARIA, or component semantics a design system declares in aria.config.ts. Ground truth, supplied.
→ format tier: auto-fix
Meaning-preserving, subtractive fixes. Run on save. Fail CI. ESLint/oxlint fix; Biome safe.
The gate
A fix may be auto-applied only if its basis is native or declared.
Enforced in code, by the host’s fix model, and by tests.
Guessed semantics
inferred
A guess from signals — an onClick on a div, class names, surrounding context. Plausible. Not proven.
→ lint tier: suggestion, never silent
Located diagnostics a human approves. Never auto-applied — structurally impossible, not just discouraged. ESLint/oxlint suggestion; Biome unsafe.
The line moves: declare a component’s semantics in config and its diagnostics graduate from guess to known — from suggestion to auto-fix.
Why a formatter, not another linter
Formatters won because a class of argument went extinct.
Accessibility breaks the formatter contract the instant you guess. Put role="button" on a div and you changed behavior. Author aria-label="Close" and you asserted a fact that might be a lie — the spec is explicit that a wrong label is worse than none.
So Aria is built around one hard line. Format-tier fixes are subtractive or normalizing: they delete ARIA that is redundant or forbidden, and they normalize syntax. They never add assertions. Aria will not invent label text, alt copy, or descriptions — it flags their absence and hands the words to a human.
When it’s unsure whether a transform is format-safe, it isn’t. It’s lint.
What exists today
Eight rules, shipped and CI-gated. Nothing claimed that isn’t proven.
Three format-tier rules gate CI with auto-fixes; five lint-tier rules surface located, human-reviewed diagnostics. Both packages are live on npm at 0.2.0, with ESLint ↔ oxlint parity verified on every commit — zero drift across every fixture.
Format tier · auto-fix
- no-redundant-role — Removes an explicit role that duplicates the element's implicit role (<button role="button"> → <button>).
- no-unsupported-aria — Removes aria-* attributes WAI-ARIA doesn't support on the element's resolved role (<button aria-checked> → <button>).
- aria-syntax-normalize — Canonical lowercase for ARIA attribute names (aria-Label → aria-label) and enumerated values (aria-hidden="True" → "true").
Lint tier · human-reviewed
- interactive-role-required — Flags a generic element (div, span) with a click handler and no role, then inspects its children to decide what to say.
- control-needs-name — Flags an interactive control with no accessible name — an icon-only <button>/<a href>, an unlabeled <input>/<textarea>/<select>.
- img-needs-alt — Flags an <img> exposed as an image with no accessible name and no decorative signal.
- idref-resolves — Flags aria-labelledby / aria-describedby / aria-controls references to an id that doesn't exist anywhere in the file.
- aria-hidden-not-focusable — Flags aria-hidden="true" on a focusable element — or on a subtree that still contains one (the common modal/dropdown bug).
Every status, basis, and spec citation lives in the rules reference, mirrored from the repo’s registry.
Two surfaces, one rule set.
An ESLint plugin that also runs under oxlint unchanged, and a zero-config CLI — the exact same rule modules, with output identical to ESLint by construction.
npm install --save-dev eslint eslint-plugin-aria-a11ynpx @aria-a11y/cli check src