The plan was to offer people a free accessibility scan of their Angular app. Most real Angular apps sit behind a login, though, and the public ones are often brochure sites with little to test. So I scanned Angular apps that are public and widely used instead, to see what the ecosystem looks like.
Two things came out of it. The raw scanner reported on the wrong page three times. And when it was right, it told me where a problem was in the DOM but not which component to fix.
Raw scanner output needs checking
I ran axe-core over interactive pages. Three times it returned a list of violations for a page that wasn't the app:
| What I scanned | What axe saw |
|---|---|
/login on the RealWorld reference app |
The app is hosted as a single-page app with no server fallback, so the deep link served GitHub Pages' "File not found" page. Eight of the contrast failures I nearly wrote up were GitHub's 404 styling. |
| Deep links into ngx-admin's dashboard | They redirect to a theme-picker splash screen. My first pass returned 484 violations, all from that splash screen. I only reached the dashboard after clicking a theme card. |
RealWorld's live demo (angular.realworld.io) |
Three issues. The home feed said "Loading articles… No articles are here… yet." The demo's backend has no data, so I had scanned an empty shell. |
In each case the number looked believable and meant nothing. I caught it by reading the flagged HTML by hand: <strong>File not found</strong> gives it away.
Two smaller things came up too. Two axe builds disagreed about whether one ngx-admin button failed button-name; a stricter manual check said it did. And I discarded a "serious" aria-hidden-focus finding on cdk-focus-trap-anchor, which appears on every Angular app that uses the CDK focus trap. It's a framework pattern rather than an app bug.
A scanner tells you where to look. You still have to check what it found.
A selector doesn't say which component to fix
When axe is right, it gives you a CSS selector such as .start-search or a[href$="johndoe"] > img. That locates a node in the DOM. In an Angular app the fix lives in a component, often one reused across many pages, and the selector doesn't name it.
Angular already has that information. In a development build it exposes a debug API on window.ng that maps a DOM node to the component whose template rendered it. Pass axe's findings through that and each violation comes with a component name.
So I added a headless report mode to our open-source @ngbracket/a11y-devtools. It drives a running dev build in a real browser, runs the same axe rules, and attributes each violation to the app component that rendered it. The report is grouped by component, and a --fail-on switch makes it usable in CI. The app under test doesn't change; report mode reads Angular's debug API from outside.
It needs a dev build, because production builds don't include window.ng. That's why I could only do this for open-source apps I could run myself, and not for other people's deployed apps. The three scans above were raw axe against production. The two apps below were scanned with report mode against a dev build. The survey after them is raw axe again.
ngx-admin
ngx-admin is a widely forked Angular admin template. Against its dashboard, report mode found 83 violation instances across 9 distinct rules, each attributed to a component.
The header search button has no accessible name, so a screen reader announces it only as "button". Raw axe identifies it as .start-search. Report mode attributes it to HeaderComponent, and shows the chain of Nebular components between the button and the header:
NbButtonComponent › NbSearchComponent › NbActionComponent › NbActionsComponent › HeaderComponent
It walks past the third-party UI components to the app component you would open to fix it. The rest of the run was sorted the same way:
| Finding | Component | What it is |
|---|---|---|
Unlabelled sidebar-toggle link |
HeaderComponent |
A template bug |
| Unlabelled social links | FooterComponent |
Demo content you wouldn't ship |
| 25 "content outside a landmark" warnings |
OneColumnLayoutComponent, via Nebular's menu |
A gap in the app's layout shell |
The search button comes from Nebular
The component chain also showed that the search button comes from Nebular's nb-search component, which ngx-admin uses. In Nebular's source, nb-search renders its open and close buttons as icon-only <button> elements with no accessible name. Every ngx-admin fork inherits that.
A look through Nebular's component templates found two aria-label attributes in the whole library, and icon-only controls without names in nb-window, the calendar's month arrows, the flip and reveal cards, and nb-chat. Attribution showed which layer the bug belongs to as well as which component, so I reported it to Nebular.
RealWorld
RealWorld is the reference app many developers copy when they learn Angular. Its live demo showed three shell-level issues because the feed was empty. Running the source in dev with real content gave a fuller picture.
image-alt and link-name both trace to ArticleMetaComponent, the author byline on each article card:
ArticleMetaComponent › ArticlePreviewComponent › ArticleListComponent › HomeComponent
It's rendered in every preview on the page. The author's avatar has no alt and the author link has no text. axe reports eight instances: four images without alt text and four empty links. That's one component with one mistake, rendered on every card, and one fix covers all eight.
A quick survey of other apps
For breadth, I ran raw axe over a few more public apps. These are production builds, so there's no attribution, and the same caution about raw numbers applies.
OpenProject is an actively maintained product. On its public work-package table, the coloured work-package type labels fail contrast. The "Task" label is #08a5df text on white, a ratio of 2.82:1 against the 4.5:1 minimum in WCAG 1.4.3. A sortable column header also has no text.
On Angular's own sites, a raw scan flagged colour contrast on angular.dev and a group of aria-allowed-role instances on the Angular Material docs. I'd verify those before filing anything.
On counting: report the number of distinct rules as well as node instances. Most of ngx-admin's 83 instances are one landmark rule firing many times. The instance count shows how noisy a page is; the distinct-rule count is closer to how many fixes it needs. Our report shows both.
What kept coming up
Across OpenProject, RealWorld and ngx-admin, the same kinds of problem recurred:
- Landmark structure: no
<main>, and content outside any landmark. - Accessible names: icon-only buttons and links with no name.
- Basics:
langon<html>, empty table headers, colour contrast.
Few of these are unusual widget problems. Most come from how components are put together into pages, or from one shared component that every page reuses. A page scan reports these as selectors. Knowing the owning component means you fix it once instead of on every page.
Where NgBracket fits
NgBracket's component packs are built for this kind of gap. A pack ships an assembled piece of UI, such as a data table, a login flow or a dashboard shell, with the landmarks, accessible names, keyboard support and focus management already in place. They're built to support WCAG 2.2 AA. Every change is checked in CI with axe and accessibility-tree snapshots, and the interactive components also have automated NVDA checks.
@ngbracket/a11y-devtools is the tool I used here. It's free, open source and runs only in development. Add the provider for a live overlay while you work, or run report mode over your dev build in CI, and each finding names its owning component.
Notes
Automated tools like axe cover roughly a third of WCAG. Keyboard and screen-reader behaviour still need testing by a person.
Every finding here is from September 2026, on the pages and builds I tested. I sent fixes where I could: pull requests to RealWorld (#363) and ngx-admin (#6073), and an issue about the missing names in Nebular (#3318). OpenProject tracks issues on its own instance rather than GitHub.
If you'd like a scan like this of your Angular app, get in touch, or start with the free packs. No card is needed.
Top comments (1)
Mapping axe findings back to specific Angular components is a massive headache because the DOM often loses the context of the component tree once it's rendered. I've run into this when trying to automate accessibility audits in CI/CD pipelines. If you can't pinpoint exactly which selector or component instance triggered the violation, developers usually end up ignoring the report because it's too time-consuming to hunt down the source code. One approach that worked for me was injecting custom data attributes during the testing phase to act as stable identifiers for the component boundaries. It adds a bit of overhead to your test environment, but it makes the feedback loop significantly more actionable for the team.