Every accessibility tool I have used reports violations like this: That selector is correct.
It is also useless.
It describes the rendered DOM, and I do not write rendered DOM — I write templates.
Somewhere in a few hundred files there is an that produced it, and finding it is manual work: grep for , get forty hits, open them one by one, compare surrounding markup until something matches.
Multiply that by sixty violations and the scan stops being useful.
Not because it is wrong, but because acting on it costs more than ignoring it.
React solved this years ago If you write JSX, puts a on every element at build time — file, line, column.
That is how React DevTools can jump you straight to source, and how error overlays point at the right line.
Angular has no equivalent.
The compiler knows the position of every element in every template: it has to, to report template errors.
But nothing carries that knowledge into the DOM.
So I built the bridge. hands you the positions exports , the same entry point uses.
Give it a template string and you get an AST where every node carries a with byte offsets, lines and columns: Two things to know immediately.
The compiler counts lines and columns from zero, and every editor counts from one — so you add one, or every location you report is off by one in both axes and nobody trusts the tool again: And matters: without it the offsets you get back describe a template that is not the one on disk.
The important decision: do not re-serialise the AST The obvious move is to parse the template, add an attribute node to each element, and print the AST back out.
Do not do this.
Printing an AST means regenerating the file from a model, and that model does not capture everything a template contains — attribute quoting style, whitespace inside tags, line breaks between attributes, comments, the exact shape of a binding.
Every one of those is something you can silently change in someone else's code.
The first time you reformat a colleague's carefully aligned template, you have lost their trust permanently, and you did it for an attribute they cannot even see.
So the rewrite is pure text insertion at offsets the compiler reported.
Find where the opening tag ends, splice a string in, leave every other byte alone: Then apply the edits back to front, so each offset still refers to the original string rather than to one you have already shifted: The output is byte-identical to the input everywhere nothing was added.
That property is worth more than it sounds: it means the instrumentation cannot break a template, and you can prove it by diffing.
Three things that bit me
1.
Control-flow blocks hide their children under different keys This one cost me an afternoon.
A naive walk over silently skips everything inside , , and — those blocks are not elements, and their children live under different property names depending on the block. uses . uses , and .
And uses — not , which is what I assumed, and what made six elements vanish from my output with no error at all: The failure mode here is what makes it dangerous: nothing throws.
You just get fewer results, and unless you have a fixture with a known count you will never notice.
- does not work The natural way to identify an element node is .
It fails in a workspace where is installed more than once — the class identity differs between copies, and your check quietly returns false for everything.
Recognise structurally instead: This turned out to matter for a second reason. 's output shape is not a public contract — it changed between 19.2.1 and 19.2.2, renaming keys and breaking angular-eslint implementations in a patch release.
Structural checks survive that; and destructured key names do not.
I confined every version-fragile line to a single file for the same reason.
3.
You are editing files you did not write Instrumenting in place means that between the rewrite and the restore, someone's working tree contains modified templates.
If the process is killed — Ctrl-C, a failed build, a crash — you have left their code in a state they did not ask for.
An in-memory backup does not survive .
So the original goes to disk before each rewrite: Already git-ignored, and checked on startup: a killed run is recovered automatically the next time the tool starts.
My first design instead refused to run on an uncommitted file.
That was worse.
A scan is something you run while working, when the tree is dirty by definition, and a guard that trips constantly is a guard people disable once and never re-enable — leaving them unprotected for the case that actually matters.
Reading it back At runtime, axe-core can hand you the real element rather than a selector: From there, the violation reads its own address off the node it failed on.
No matching, no heuristic, no fuzzy correlation between DOM and source.
That last part is the design rule I would keep even if everything else changed: exact or absent, never guessed.
A developer sent to the wrong line loses more time than a developer sent nowhere at all, and one bad location poisons trust in every good one.
What this does not solve Anything the runtime produces has no source line, and pretending otherwise would be a lie: an avatar coming from an API, the contents of a third-party component.
Those get reported separately rather than pinned to an approximate location.
Locations only exist on an instrumented build, so scanning an arbitrary production URL still gives you violations — just no files.
The tool I packaged this as rgaa-source, which runs axe-core plus some rules of its own against your built app and reports every violation with its file and line.
It also maps findings onto RGAA 4.1.2, the French accessibility reference frame, because the European Accessibility Act made that a legal question for a lot of people this year.
MIT, runs locally, nothing hosted.
Verified on Angular 15 through
22.
But the bridge is the part I think is worth sharing regardless of the tool wrapped around it — it is about two hundred lines, and nothing about it is specific to accessibility.
Any analysis that runs against the rendered DOM could use the same trick to point back at source.
If you try it on a real Angular project, I would genuinely like to know when a location comes out wrong.
That is the failure mode I cannot find on my own, and it is the one that matters most.