Getting started

Quality

What gates a change to ngwr — 8 checks on every pull request, 4,084 test cases and three accessibility sweeps — and an honest account of what those gates cannot see.

Counted, not claimed

Every number on this page is written by scripts/gen-quality.ts, which counts the repository during the showcase build. Nothing here is typed in by hand — that is the only way a page like this is still true a year after it was written.

Entry points
202
Components + directives
173
Test cases
4,084
Spec files
238
Test harnesses
103
Documentation pages
208
Runtime dependencies
1

Every counter says what it measured. Test cases are declared it() call sites, which equals the real figure only while nothing mints cases at runtime — so a parameterised form (it.each) or a call site inside a loop makes the generator print the file and line and label this figure a floor, and the page then reads “at least”. A spec file that yields zero cases still fails the build outright, because that is not a way of writing tests, it is what a broken comment-stripper looks like. “Documentation pages” counts routes that render one: the layout shell and the ** 404 both declare a component and neither is a page, so neither is in the 208. The version is read from projects/lib/package.json and cross-checked against the NGWR_VERSION constant, which is a separate copy; a mismatch fails the build rather than picking a side. The single runtime dependency is tslib.

The gates on every pull request

These run on every push to a pull request, in this order. The last two read the prerendered site, so they cannot start before it exists.

All 8 of them have to be green before anything merges, and the publish job re-runs the lint and the library build again before it ships a tarball.

Gate
What it catches
lint ESLint, Stylelint and four repository gates in one chain — colour-list parity, unexplained physical CSS, the registry format, dead design tokens. Every stage is listed below.
test:coverage The vitest suite, with coverage. Specs sit beside the code they cover and assert the rendered DOM — roles, ARIA state, and the .wr-* classes, which are public API — rather than component internals.
check:api-docs A documented input the component no longer has, a default the docs invented, a page with no API table at all. Every hand-written table is compared against the signal API in the source.
check:llms The generated AI assets — llms-full.txt and the agent skill — against coverage floors. Missing frontmatter, or a catalog table with nothing but a header, fails the build.
build:lib ng-packagr over every secondary entry point, then the schematics, the MCP server and the AI assets. An entry point that does not compile in isolation fails here and nowhere else.
build:showcase Every documentation route prerendered in Node. SSR breakage is a red build rather than a silent degrade: a component that touches the DOM outside afterNextRender cannot reach a release.
check:theme wrThemeTokens(), the runtime palette recipe, against the compiled stylesheet — token by token. Two implementations of one recipe drift the moment either is edited.
check:a11y axe over the prerendered HTML: accessible names, ARIA validity, roles, id references, landmark and heading structure. Fails on any serious or critical violation, and the baseline is empty.

Trust the exit code, not the output.

pnpm lint is a chain of stages joined by &&, and the first one prints “All files pass linting.” on its way to a later stage that fails. A silently failed lint stage slipped past exactly once, and it blocked a publish.

Inside pnpm lint

ng lint is only the first stage. The rest are gates a linter has no opinion about: they read the repository for the kind of drift that leaves both halves independently valid while the pair is wrong.

Stage
What it catches
ng lint ESLint over the library, then the showcase — templates included.
eslint scripts The same rules over the build and release tooling — TypeScript that never ships to npm, and that every gate on this page runs through.
lint:styles Stylelint over every stylesheet in both projects.
check:colors The TypeScript colour list against the SCSS palette map. They drifted once: v8 shipped --wr-color-info and its whole modifier class while color="info" stayed a template type error.
check:rtl A direction-dependent CSS property written in physical form with no rtl-ok: reason above it. Plenty of them are correct — the rule is that the reason is written down.
check:registry The open registry items, their entryPoints against the real catalog, and schema.json against the validator that enforces it.
check:tokens A --wr-* token nothing paints with. A say-why gate rather than a do-not gate: an intentionally unused token carries unused-ok: and the reason.

What runs nightly instead

The rest of the sweep needs a real browser, real CSS and several hundred page loads. On the pull-request path that roughly tripled the job, and what it catches is drift in painted output rather than the kind of break a single review has to be told about — so it runs on a schedule instead.

Gate
What it catches
build:showcase Not a check. The three below read dist/showcase and cannot start without it.
check:contrast axe's color-contrast and target-size rules in a real Chromium, both themes, every canonical route — the two rules check:a11y has to switch off.
check:state-a11y The full axe rule set inside a state you have to create: a hover, a focus ring, an open overlay. Neither static gate can reach one.
check:rtl-layout Every route rendered both ways, failing only where the RTL pass overflows sideways and the LTR pass does not. Differential, so there is no baseline of pixel positions to rot.

The trade is worth saying out loud: a green pull request says nothing about painted contrast or RTL overflow. If you are working on the library and you touch a token, a tint, or anything that paints text on an intent, run pnpm check:contrast yourself rather than waiting for the morning.

Three accessibility gates, and what each one cannot see

Three sweeps, because no single one of them reaches the whole surface. The last two rows of each block are the ones that matter: what the gate is blind to, and what it has already been told to accept.

check:a11y

Runs
Every pull request. axe over the prerendered HTML, in JSDOM.
Sees
The structural half: accessible names, ARIA validity, roles, references to ids that must exist, landmark and heading structure. It fails on any serious or critical violation and the baseline is empty. That class is real — the audit it was written after found an unnamed speed-dial trigger, icon-only segmented options, and a dialog promising an aria-modal it did not honour.
Cannot see
Anything that depends on computed style. JSDOM loads no stylesheets and does no layout, so color-contrast and target-size are switched off explicitly rather than left to report noise. And prerendered HTML holds no hover, no focus ring and nothing inside an overlay.
Already accepted
Nothing. scripts/a11y-baseline.json is {}, so there is no allowance to hide a regression in — which is exactly why the two blocks below have to state theirs.

check:contrast

Runs
Nightly. A real Chromium, real CSS, both themes, every canonical route.
Sees
Exactly the two rules the structural sweep has to disable. It reports axe's own measured ratio instead of re-deriving one, emulates prefers-reduced-motion so an animation caught mid-flight cannot report a failure that describes a single frame, and pins the clock so a demo built from new Date() cannot move its own route count overnight.
Cannot see
A page at rest is all it measures, so hovers, focus rings and overlay contents stay out of reach. And it gates on route counts per rule, not on nodes — a brand-new violation on a route that already fails passes silently. Not hypothetical: deepening the dark primary broke wr-calendar__day--today at 4.06:1 on a route already baselined for something else, and the sweep printed “no new violations”. Change a token, measure the elements you changed.
Already accepted
Four entries in scripts/contrast-baseline.json: color-contrast on four light routes and two dark ones, target-size on one route in each theme. The worst is wr-calendar__day--out-of-month at 2.23:1 — enabled buttons the arrow keys reach, so the exemption for inactive controls does not apply to them, and it is opacity: 0.55 rather than the colour that drops the ratio. The rest are the carousel's 8×8 pagination dots, the swatch grid on Colour tokens (which labels every shade of a ramp with a token calibrated for the base shade), and disabled controls axe cannot recognise as disabled because the attribute sits on a custom element. Every entry carries the reason it was accepted.

check:state-a11y

Runs
Nightly. A curated table of states, driven in a browser, with the full axe rule set inside each one.
Sees
The states a user has to create: a hover, a focus ring, an open dialog, a select panel, a context menu. Seven real AA failures were found there by hand, every one long-standing and every one under a green run. Colour is not all of it — the structural rules had never run inside an overlay either, which is how a role="button" span nested inside a real <button> survived in the window taskbar, on a tab that only exists while a window is minimized.
Cannot see
Whatever is not in the table. A curated list that stopped growing looks exactly like one that covers the catalog, so a full run prints its own coverage — state-dependent classes in the built stylesheet against the classes it actually painted — and a state that failed to paint fails the run, because a clean axe result over an element that never rendered is indistinguishable from a pass.
Already accepted
Ten entries in scripts/state-a11y-baseline.json, and they are four decisions rather than ten problems: the same out-of-month calendar days as above (2.23:1 light, 3.11:1 dark) reached through two separate states, and target-size on the window chrome's 14×14 traffic-light buttons, the carousel dots again, and wr-event-calendar's 20px chips. Each is a design call with the alternative costed in the file — not a finding nobody looked at.

And one failure no gate reports at all.

--wr-color-outline measures 1.48:1 in light and 1.41:1 in dark, against the 3:1 WCAG 1.4.11 asks of anything that identifies a control. Fifteen of its declarations are control boundaries where the criterion applies; the rest are cards, dividers and table rules, which it does not reach. Both fixes were costed — darkening the one token repaints every rule that uses it, and splitting out a control-border token leaves control edges visibly darker than every line beside them — and the maintainer chose to keep the hairline. No gate reports it, because axe ships no non-text-contrast rule.

What the tests cover, and what they do not

The suite is broad. It is not uniformly deep, and the shallow parts are worth naming.

4,084 cases across 238 spec files, and 202 of the 202 entry points own at least one. Read that second figure narrowly: it is file-level coverage of the catalog, not branch coverage of the code. Specs sit beside what they cover and assert the rendered DOM — roles, ARIA state and the .wr-* class names, which are public API because every component ships ViewEncapsulation.None. A spec that reaches into component internals passes straight through the kind of change that actually breaks people.

103 CDK test harness classes ship alongside, published from 70 ngwr/<name>/testing entry points, so your own tests can drive a select or a date picker without knowing its markup. Four entry points deliberately have none — three that draw into a canvas jsdom hands no context for, and a thin wrapper over the CDK's virtual scroller — and the reason is the same each time: every honest method would answer identically for a working component and a broken one.

Which is also the first hole. jsdom has no drawing context and no layout — every rect is 0×0 — so the canvas and WebGL components can never assert anything painted. All six install a recording context and five an error recorder besides, which carries their specs past the early return to frame counts, particle counts, a var(--wr-…) colour resolved against the host, gravity bringing a particle back down, and teardown. Painted output itself is not reachable, and neither is anything measured in pixels.

The second hole is mode coverage inside components that are covered.wr-table has column pinning, drag-reorder, grouping, tree rows, expandable rows, CSV export and a virtualized body on one host; a spec on the table says nothing about tree rows unless it exercises them, and not every spec does. A green suite here means no covered behaviour regressed — not that every behaviour is covered.

The third is classic-forms interop, and it is the one worth knowing about if you are weighing ngwr against a library that still ships a ControlValueAccessor. Angular 22 synthesises the accessor for a signal-forms control, so [(ngModel)] and [formControl] reach the same value model — but in this suite that is doctrine more than coverage. [(ngModel)] is bound to a real ngwr control in exactly one spec (wr-textarea), and no spec binds [formControl] to one at all — the reactive-forms specs that exist drive native <input>s through WrValidators. Angular's own tests cover the synthesis; nothing here covers it per component.

Breaking changes, and which ones a codemod can honestly repair

A major ships an ng update codemod when a codemod can actually repair what broke — and deliberately ships none when it cannot.

That rule is the whole table. v12 moved three import paths, which is exactly what a codemod can move without reading the code around it. v10 and v11 changed painted colour, which no codemod can repair — and shipping an empty one would have told you your visual regressions were handled when they were not.

Major
What broke
Codemod
v12 The three date entry points nested under ngwr/date. readI18nText() returns a Signal, so every read needs a call. Partial
v11 Five colour intents deepened past the point where a filled control takes a white label instead of a black one. None
v10 Contrast on the -contrast tokens, table header casing, tooltip theming. None
v9 A checkbox’s group identity moved from value to checkboxValue. Lucide icon keys register verbatim. info joined the colour union. Yes
v8 Density values renamed from compact / default / comfortable to sm / md / lg. Pagination dropped xs and xl. Two unreliable components removed. Yes
v7 Ten standalone entry points consolidated into shared components with modes — the autocomplete became a wr-select, the tooltip a wr-popover. Yes

The partial is deliberate too. v12's other break — readI18nText() returning a Signal<string> — is not codemodded, because adding () means knowing which identifiers hold the result, and a wrong guess is a silent behaviour change. The type error names every call site instead. The migration guide documents both halves, and every break back to v7.

What this does not buy you

All of the above is true, and none of it is the whole picture.

Maintainers
One. The machinery on this page exists because a single person cannot re-check 202 entry points by hand, not because there is a team behind it. Reviews are self-reviews, and the gates are what stands in for a second pair of eyes.
Ecosystem
Small. There is no Stack Overflow tag worth searching, no paid theme, no third-party component built against ngwr. If you hit something the docs do not cover, the source and the issue tracker are the support channel — the whole library is MIT and readable.
Commercial support
None. No SLA, no contract, no paid tier, no roadmap commitment you can hold anyone to. Every gate described on this page runs in the public repository, so a fork is a real option if it comes to that.
Stability
The catalog moves. v12 is the current line, and the table above is the honest record of what each step cost. Pin the major, read the migration guide, and do not assume an upgrade is free because the gates are green — two of the last three majors broke things no codemod could repair.
Design
One person's taste. The tokens are documented and all of them are overridable, but nothing here went through a design review, because there is nobody to hold one with.

See also