DEV Community

Manichandra Sajjanapu
Manichandra Sajjanapu

Posted on

One ARIA bug, a million apps: meet aria-reach

Most accessibility tools audit your application. But in a modern JavaScript app, the accessibility of what ships is mostly decided one layer down — in the shared component libraries you import: the UI kit, the rich-text editor, the media player, the form framework.

That layer is where accessibility bugs scale. A single misconfigured aria-* attribute in a popular library can propagate into downstream apps that import it. A wrong aria-live value in a media player can affect sites that embed it. The flip side is the opportunity: a confirmed upstream fix can benefit many consumers as they adopt the corrected release.

aria-reach is a small open-source analyzer built around that idea. It scans for ARIA anti-patterns in component libraries (and in any live page), attributes findings to their likely upstream source, and — the part I haven't seen elsewhere — ranks them by reach, so you spend effort where it helps the most assistive-technology users.

npm install -g aria-reach
aria-reach scan src/
Enter fullscreen mode Exit fullscreen mode

The four classes of anti-pattern

aria-reach organizes findings into a four-class taxonomy. Each class is grounded in a real, concrete defect pattern from a widely used library.

Class I — Decorative Noise Injection. Decorative elements leak into the accessibility tree and get announced as content. Classic case: breadcrumb separators (/, ) read aloud between every link. The fix is aria-hidden="true" on the decorative node.

Class II — Live-Region Urgency Miscalibration. Routine, non-urgent updates announced with aria-live="assertive" (or role="alert"), which interrupts whatever the screen reader is currently saying. A media player that re-announces description text on every update becomes unusable. Routine status belongs in a polite live region (aria-live="polite" / role="status"); assertive is for genuinely time-critical alerts.

Class III — Widget Role Contract Violations. A widget breaks the W3C role contract that assistive tech relies on. Two common shapes:

  • A calendar day cell using aria-pressed — so a screen reader announces "button pressed" instead of "selected." A day in a grid is a selection, so it needs aria-selected.
  • A custom dropdown built from divs with no role="listbox"/role="option", no aria-selected, and no keyboard support — invisible to AT as a selectable list.

Class IV — Async State Desynchronization. The accessibility state and the actual state drift apart across async boundaries — e.g., a form submit firing before async validators resolve, so the user is told the form is valid when it isn't.

Each rule maps to a specific WAI-ARIA 1.2 / WCAG 2.1 success criterion (mostly SC 4.1.2 Name, Role, Value and SC 4.1.3 Status Messages), and each is grounded in a real upstream contribution.

What makes it different: reach scoring

Finding ARIA issues is the easy part; prioritizing them is the hard part. axe-core and friends give you a flat list per page. aria-reach weights each finding by a Library Reach Index — roughly weekly npm downloads × estimated downstream deployments — so a defect in a library pulled millions of times a week outranks a one-off in your own code. You fix the things that touch the most people first.

It also attributes findings to their likely origin (PrimeNG, Angular Material, Quill, Video.js, USWDS, MUI, …), so a finding becomes an upstream fix opportunity — the force multiplier — rather than a local patch.

Two ways to scan

Static — templates, including Angular inline templates extracted from .ts/.js with line numbers mapped back to source. It understands Angular binding syntax ([attr.aria-hidden]="expr" counts as handled, and unknowable bound values are never false-flagged):

aria-reach scan src/            # .html + inline Angular templates
aria-reach scan src/ --json     # machine-readable, for CI
Enter fullscreen mode Exit fullscreen mode

The CLI exits 1 on any error-severity finding, so it can gate a CI job. As a worked example, scanning PrimeNG's own library source surfaces 172 candidate findings across 51 component files — a useful map of where to look (you still validate a sample by hand; the goal is to point effort, not to auto-merge).

Drop it into CI with the GitHub Action — it fails the build on error-severity findings:

- uses: manichandra/aria-reach@v0.1.2
  with:
    path: src              # fail-on-error: false for report-only
Enter fullscreen mode Exit fullscreen mode

Runtime — the runtime-detectable rules (Classes I–III) run against the rendered DOM of any app (React, Vue, Angular, vanilla — at runtime it's all DOM). Recognized DOM fingerprints provide heuristic, likely-origin labels that require confirmation. Paste the built browser bundle into a DevTools console, or load the browser extension:

ariaReach.scan();     // grouped report in the console
ariaReach.summary();  // counts by class + reach
Enter fullscreen mode Exit fullscreen mode

Try it (and break it)

It's MIT-licensed, Node ≥ 18, and archived with a DOI so it's citable:

It's the reference implementation of an ARIA anti-pattern taxonomy I'm writing up in a paper (under review; preprint to follow). If you maintain a component library, I'd love a scan result or a counter-example that breaks a rule — open an issue or a PR. The most useful thing you can do is point it at a library you depend on, confirm the highest-reach finding, and propose the fix upstream.

Built and maintained on personal time. Feedback and contributions welcome.

Top comments (0)