Getting started

Quality

What gates a change to ngwr — 9 checks on every pull request, at least 5,430 test cases and three accessibility sweeps — and an honest account of what those gates cannot see.

Counted, not claimed

The counters below are written by scripts/gen-quality.ts, which counts the repository during the showcase build, and every sentence further down that repeats one of them binds the same value. None of them 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
229
Components + directives
179
Test cases (at least)
5,430
Spec files
303
Test harnesses
106
Documentation pages
224
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 224. 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 9 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 the repository gates in one chain — colour-list parity, unexplained physical CSS, the registry format, dead design tokens and colour-only state rules. 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.
check:css-vars The --wr-<name>-* hooks each component page lists, regenerated from the stylesheets and compared against the committed copy. A hook a component grows cannot ship without its row.
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.
check:color-only A state or intent modifier whose own declarations are all colour, with no color-ok: reason above the selector. WCAG 1.4.1 has no axe rule, so this reads the stylesheets instead — @each loops included.
check:selectors tsx scripts/gen-selectors.ts --check

What runs nightly instead

Most of this 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. The first two are the exception and are not a second opinion on the same thing: they are the unit suite and the library build re-run on Node 22 and 24 — the LTS lines engines still accepts, which nothing else exercises now that every other job runs 26.

Gate
What it catches
test ng test lib
build:lib tsx scripts/build-lib.ts
build:showcase Not a check. The browser checks 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:layout The measured box of a set of load-bearing components in both themes, against a committed baseline. It catches a token or density change that silently resizes controls, and says nothing about colour, shadows or radii.
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. It also measures what axe never does: placeholder text. axe reads an empty field's typed-text colour, which is how input placeholders at 2.91:1 passed as 17.85:1, so every field showing its placeholder gets that colour copied onto its text for one scoped pass.
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
The entries in scripts/contrast-baseline.json, one per rule, theme and prefers-contrast mode, each allowing a number of routes rather than of nodes. 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), disabled controls axe cannot recognise as disabled because the attribute sits on a custom element, and the table header's sort and filter controls on a fine pointer — 12×12 apiece and 6px apart, a decided trade rather than debt, because widening them for every pointer moves the caret and the funnel apart in every desktop table. 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
The entries in scripts/state-a11y-baseline.json, one per state and theme, come down to a few decisions rather than a pile of 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 and on the carousel dots again. 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.

Colour is never the only channel — and no runtime gate can check that.

WCAG 1.4.1 has no axe rule, and the nine intents cannot carry it on their own: six share one luminance to within 1.06:1, and success against danger is 1.004:1, because they were tuned to take the same white label. So the components that report a state draw a shape for it — alert and toast a glyph per type, timeline a silhouette per dot — while the colour of a tag, a badge or a typography tone is decoration or category, and the words have to say the state; a tag that must stand for a status on its own takes an icon. A source check, check:color-only, holds every colour-only rule in the library to a written reason. It reads ngwr's stylesheets, not your templates.

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.

At least 5,430 cases across 303 spec files, and 229 of the 229 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.

106 CDK test harness classes ship alongside, published from 72 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. No accessor is created: Angular's NgModel / FormControlDirective drive the control's value or checked model directly. That bridge does not apply template validator directives — a required on an ngwr control produces no error and the form submits, so validators belong on the FormControl — and it ignores updateOn. Only some value controls are bound through the classic forms API in a spec: wr-checkbox and wr-select ([(ngModel)] and [formControl], plus the validator drop), wr-segmented (the same two bindings), wr-textarea ([(ngModel)], and [formControl] inside a form field), and wr-date-picker and wr-date-range-picker ([formControl]). The other reactive-forms specs drive native <input>s through WrValidators. Angular's own tests cover the bridge; the forms guide is where its semantics are written down.

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
v14 Six names moved to one word per concept — closeable to closable, totalItems to total, currentPage to page, and [wrInput]’s wrSize to size. Router integration became opt-in for wr-loading-bar and routed tabs. A named date format refuses input it cannot read, and wr-pagination ofLabel is gone. Partial
v13 readonly and invalid reach every control, and [id] on checkbox, radio and switch stopped landing on the host — getElementById now returns the inner input, which is what the input always documented. Reports
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

Both partials are deliberate. 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. v14 rewrites its six renames and only reports the rest: opting a loading bar or a routed tab strip back into the router is a decision about a bundle, not a substitution, and the stricter date parsing has no token to detect. The migration guide documents every half, 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 229 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. v14 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 — each of the last three majors broke something no codemod could repair. One thing has changed structurally, and it is the one worth knowing before you write a range: a breaking change can no longer ride a minor or a patch — the release script refuses the bump, which it did not when 12.2.0 shipped one. Versioning & support has the rule, the support window, and what the open-ended Angular peer range does and does not promise.
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