Introducing a11y-oracle: Testing That Goes Beyond Static Analysis

Published on Mar 05, 2026

tldr;

If you've worked on a project that takes accessibility seriously, you've probably run into a frustrating pattern. You install axe-core, wire it into your test suite, and within a few runs you're staring at a list of results labeled "Needs Review." Not passed. Not failed. Just... incomplete. And every single time your CI runs, there they are, cluttering your results, requiring someone to manually check whether that color contrast issue is real or a false positive.

I've been down this road more than once, and it always bugged me. These aren't theoretical edge cases — they're real WCAG requirements around color contrast, focus indicators, keyboard traps, and target sizes that axe-core simply can't verify by looking at the DOM alone. So I built a11y-oracle to fix it.

What is a11y-oracle?

At its core, a11y-oracle is a set of npm packages that use the Chrome DevTools Protocol (CDP) to test accessibility in ways that static DOM analysis can't. It does three main things:

  1. Speech assertions — Reads the browser's accessibility tree and produces standardized speech output (like what a screen reader would announce), so you can assert exactly what assistive technologies will communicate to your users.
  2. Keyboard and focus testing — Sends real keyboard events through CDP, then validates focus indicators meet WCAG 2.4.12, checks tab order, and detects keyboard traps.
  3. axe-core incomplete resolution — Takes those "Needs Review" results from axe-core and actually resolves them using screenshots, keyboard interaction, and CDP inspection.

It ships as 9 packages under the @a11y-oracle scope, but for most people you'll only need one or two. There are drop-in plugins for both Playwright and Cypress.

The Problem with Static Accessibility Testing

Let me be a little more specific about the problem. Tools like axe-core do an incredible job of catching accessibility issues through DOM inspection. Missing alt text, incorrect ARIA attributes, heading hierarchy problems — axe catches all of these reliably. But there's a whole category of WCAG requirements that need more than the DOM to verify.

Take color contrast, for example. axe-core can compute contrast ratios for simple cases where the text color and background color are straightforward CSS values. But what about text on a gradient background? Or text overlapping an image? axe-core marks these as "incomplete" because it literally can't determine the answer without seeing the rendered pixels.

The same goes for focus indicators. WCAG 2.4.7 (and the stricter 2.4.12 in WCAG 2.2) require that focus indicators are visible and have sufficient contrast. But you can't check that without actually focusing the element and comparing what changed on screen. axe-core can see that an element has an outline style, but it can't tell you whether that outline is actually visible against the background.

And then there are interactive checks — does a skip link become visible when focused? Does content that appears on hover stay visible when you move the mouse to it? Can you dismiss it with Escape? These all require actual interaction with the page.

The Approach: Use the Browser Like a User Would

The key insight behind a11y-oracle is that the Chrome DevTools Protocol gives us everything we need to test these things programmatically. CDP lets us:

  • Read the full accessibility tree (the same data screen readers consume)
  • Send native keyboard events (not synthetic JavaScript events — real hardware-level key presses)
  • Take screenshots and analyze pixels
  • Read computed CSS values from any element
  • Execute JavaScript in the page context

So instead of looking at the DOM and guessing, a11y-oracle interacts with the page the way a real user would, and then verifies the result.

Speech Assertions: Test What Your Users Hear

The part of a11y-oracle I'm most excited about is the speech engine. It reads Chrome's accessibility tree and produces a standardized string in the format:

[Computed Name], [Role], [State/Properties]

So for a button like , you'd get Products, button, collapsed. For a navigation landmark like