Skip to content

About component-to-host resolution

Modern accessibility rules are written about host elements — the intrinsic building blocks the browser and assistive technology actually understand: button, input, a, label, img, textarea, select. But almost nobody writes those tags directly anymore. They write <Button>, <TextField>, <Checkbox> — design-system wrappers that render a host somewhere inside. This gap between the component a developer writes and the host a rule reasons about is the problem component-to-host resolution exists to close, and the way b8e closes it says a lot about what the static scanner values.

An accessibility rule like “every input needs an accessible name” is only meaningful once you know which JSX in the source is an input. When the source says <TextField label="Email" />, a naive scanner has two bad options: treat every capitalized component as opaque and check nothing (misses real defects), or guess that TextField is an input and check it (invents false ones when the guess is wrong). Neither is acceptable. The static lane’s whole claim to being a trustworthy gate rests on not crying wolf, so the scanner needs a principled way to answer “what host does this wrapper render?” — and to admit when it cannot.

That admission is the design’s center of gravity. A wrapper the scanner can’t confidently map is reported opaque, not mapped to a best guess. The guiding principle is honesty over coverage: it is better to say nothing about a component than to say something wrong about it. Everything below is machinery in service of that principle. It is the same instinct that makes the static lane deterministic in the first place — see About the two scanning models.

Four tiers, cheapest and most certain first

Section titled “Four tiers, cheapest and most certain first”

Resolution is a ladder. Each rung is more expensive and more general than the one above it, and each only runs when the rung above came up empty. The order is not arbitrary — it is sorted by certainty and cost together, so the scanner spends the least effort that still yields a confident answer.

  1. Declaration. If a component’s mapping is declared up front — the accessibility contract says this wrapper injects a label, or renders as this host — that declaration wins. It is the most authoritative signal because a human stated it deliberately; there is nothing to infer.

  2. Registry. For the design systems most codebases actually use — Radix, MUI, and the like — the wrapper-to-host mapping is known in advance and shipped as pure data. <Radix Label.Root> is a label; <MUI TextField> is an input. This is the fast path: a table lookup, no source reading, and adding a library is a new data row rather than new code. It is deliberately conservative — only wrappers with one unambiguous host are listed. Layout and container components (Card, Dialog wrappers) are intentionally absent, because mapping them to a single host would be a lie; they fall through instead.

  3. Source-trace. For any wrapper the registry doesn’t know, the scanner reads the wrapper’s own definition from source, using the customer’s real tsconfig resolution — the same paths and aliases their build uses. It asks two narrow questions: does this component render exactly one host element, and does it forward its props? Only if both hold does it infer the mapping. This is what lets the scanner work on codebases it has never seen, with zero configuration. It follows a bounded number of wrapper-of-wrapper hops and then stops; anything ambiguous returns nothing and the component stays opaque.

  4. Graph. Source-trace is a single-file tracer with a limited hop budget, so it loses the trail through barrel files, re-exports, and long forwarding chains — exactly the indirection large monorepos are built on. The graph tier resolves those same wrappers through the real import/export graph of the whole project, following an export back to the module where the host is truly defined and tracing it there. It is the heaviest tier and an enrichment layer, not a hard dependency: building the graph needs a reachable tsconfig, so on a cold or shallow checkout it simply doesn’t build, and resolution falls back to source-trace exactly as before.

Why an escalating ladder rather than one clever resolver

Section titled “Why an escalating ladder rather than one clever resolver”

One could imagine collapsing this into a single mechanism — trace everything through the graph, say, and drop the registry. The tiered shape is a deliberate rejection of that, for a few reasons worth weighing.

First, the cheap tiers are also the most certain ones. A human-authored declaration and a curated registry entry are more trustworthy than any inference, so putting them first is not just a performance trick; it is putting the highest-confidence signal in charge when it exists.

Second, the tiers degrade gracefully. Source-trace needs no configuration and runs anywhere; the graph needs a resolvable project and gives up cleanly when it can’t build. A user on a shallow CI clone still gets registry and source-trace results — they lose the graph’s extra reach, not the whole scan. A single graph-only design would turn “no tsconfig” into “no resolution.”

Third — and this is the quiet through-line — every tier shares the same exit: when in doubt, opaque. None of them upgrade a guess into a claim. The ladder widens what the scanner can confidently resolve without ever widening what it is willing to assert on thin evidence.

The honest reader should see the trade the design accepts. Reporting a wrapper as opaque means a real defect inside an unresolvable component can go unreported by the static lane — a false negative in exchange for avoiding false positives. That is a considered choice, not an oversight: a static gate that occasionally misses is one teams keep running, while one that regularly invents findings gets disabled within a week. And the miss is not the end of the story — components that only resolve at runtime are exactly what the live-DOM and audit lanes are for. Resolution failures in the source lane are a reason to render the page, not a reason to guess about it.