Skip to content

About the two scanning models: static source vs live DOM

b8e looks for accessibility defects in two fundamentally different ways, and the shape of the command you run decides which one you get. b8e scan . reads your source files and never opens a browser. b8e scan --url, --crawl, and --story drive a real browser and inspect the rendered DOM. The two lanes answer different questions, and understanding why they are separate explains a lot about where each one is trustworthy and where it is blind.

A static source scan asks: what does the code say? It parses your components — the JSX, the markup, the framework templates — and reasons about the accessibility contract each element declares at rest. An <img> with no alt, a control with no accessible name, a heading level that skips: these are properties of the source, visible without ever running anything.

A live-DOM scan asks a different question: what does the page actually become? It launches a browser, navigates, lets the framework hydrate and the JavaScript run, and only then reads the accessibility tree the user’s assistive technology would see. That tree is the product of routing, data fetching, client-side rendering, portals, and conditional branches — none of which the source alone can settle.

These are not two views of the same fact. They are two facts. A defect can be plainly wrong in the source and disappear at runtime (a value injected by a wrapper), or be invisible in the source and appear only once the page renders (a modal that traps focus, a live region that never announces). Neither lane is a superset of the other, which is the core reason b8e keeps them apart rather than pretending one command covers everything.

The static scan’s defining property is determinism: the same source produces the same findings, every time, with no network, no browser, and no flakiness. That is a deliberate design choice, not an accident of implementation. A source scan is fast enough to run on every commit, reproducible enough to gate a pull request, and honest enough that a green result means this input is clean rather than this run happened to pass.

Determinism also sets the boundary of what the lane can claim. Reasoning about source means reasoning about code the scanner can read and resolve. When a component’s real behavior lives behind a boundary the source can’t cross — a value computed at runtime, a wrapper whose host it can’t trace — the honest move is to report the element as opaque rather than guess. That instinct, honesty over coverage, runs through the whole static lane; it is discussed further in About component-to-host resolution.

If the source lane is deterministic and cheap, why keep a slower, browser-driven one? Because a large class of real accessibility failures simply do not exist until the page runs. Focus order, dynamic ARIA state, announcements from live regions, contrast against actually-painted pixels, the behavior of a route after client navigation — all of these are runtime properties. No amount of source reading recovers them, because the source is not where they happen.

The --url, --crawl, and --story flags are three ways into that same live-DOM lane, differing only in what gets rendered:

  • --url renders one or more specific pages and aggregates them into a single site report.
  • --crawl takes one seed URL and expands it into a same-origin page set before aggregating, for when you want breadth without enumerating every route by hand.
  • --story renders Storybook stories, which is often the most useful shape during development: components exercised in isolation, in the states their authors actually care about.

The trade-off is the mirror image of the static lane’s. Live-DOM findings are richer and closer to the user’s real experience, but they depend on a browser, a network, and a page that renders predictably — so they cost more and carry the ordinary fragility of anything that drives a real browser. For the exact flags, budgets, and defaults, see the b8e scan reference.

Where audit fits, and why it is a third thing

Section titled “Where audit fits, and why it is a third thing”

It is worth naming a third path so it is not confused with either scan lane. b8e audit runs outcome-based journeys through a real browser and, in its cli client-type, a real screen reader — it verifies whether a user can complete a task, not just whether the markup is well-formed. That is a different unit of truth again: a scan reports defects on a surface, an audit reports whether a journey succeeds. The b8e audit reference covers its options; the distinction matters here only to make the point that “does the code declare the right thing”, “does the rendered page expose the right thing”, and “can a person actually get through the flow” are three separable claims, and b8e gives each its own command rather than blurring them.

Reach for the static lane when you want a fast, reproducible gate on what the code says — the loop the scan-and-fix tutorial walks through. Reach for a DOM lane when the question is about what the page becomes at runtime, which the source can never fully answer. The separation is not an incompleteness to be papered over; it is the honest shape of the problem, and each lane is trustworthy precisely because it does not pretend to cover the other’s ground.