The short version
ngwr is a signals-first Angular UI library written for one version of Angular and one way of writing forms. Nineteen value controls implement Angular's FormValueControl or FormCheckboxControl contract directly, so [formField] binds to the component with nothing in between. There is no ControlValueAccessor anywhere in the package. A count rather than "every", because one public component carries its own value model and implements neither — [wrColorPickerTrigger]. It is zoneless rather than zoneless-compatible — zero @NgModule declarations and no zone.js dependency — standalone throughout, and it ships as 202 tree-shakable entry points with tslib as its only runtime dependency.
That is the whole claim. On the rest of what a team weighs — install base, contributors, locales, paid support, catalog breadth — ngwr either ties or loses, and the sections below say which. A reader who leaves in ten seconds because this page told them the truth is a better outcome than one who adopts and churns in a quarter.
Do not use ngwr if
Read this list before the argument, not after it. Two of these are on the roadmap — locale packs and a rich-text editor — and neither is committed to; the rest are what the project is.
- You need a vendor. There is no paid support, no SLA, no consulting arm, and nobody to escalate to. PrimeNG and Nebular sell exactly that; ngwr has an issue tracker.
- You need a bus factor above one. 1,409 of 1,480 commits are by one person, under four author identities. The next human contributor has 13; the two bots have 29 each. The first commit was 2022-08-22 — nearly four years, one pair of hands.
- You need API stability across majors. Six majors shipped between 2026-06-12 and 2026-08-20, v7 through v12. Four of them carry an
ng updatecodemod; v10 and v11 changed painted colour, which no codemod can repair, and ship none on purpose rather than imply the regression was handled. - You ship in more than English or Russian. Those are the two catalogs in the box.
provideWrI18n()takes your own and the key set is gated against drift, but you will be writing them. NG-ZORRO bundles many more; Taiga UI ships 23 language packages. Locale packs are on the roadmap and are not committed to. - You need a rich-text editor, an organisation chart, or Excel export. None are here. An editor is on the roadmap, unscheduled and explicitly waiting on demand.
.xlsxexport is a standing refusal rather than a gap — it needs a third-party dependency, andtslibis the only one this package has. - You need to hire people who already know it. 151 weekly downloads on npm and 3 GitHub stars, read on 2026-08-20. Nobody's CV says ngwr.
The wedge: Signal Forms with nothing in between
Angular 22 shipped Signal Forms as stable. The FormField directive documents three ways a control can bind to a field — a native input or textarea, a component implementing FormValueControl / FormCheckboxControl, or a component providing a ControlValueAccessor — and says of the third that it “should only be used for backwards compatibility with reactive forms”.
Every established Angular UI library grew up before that second path existed, and the ones whose published types show it bind through the third. Here is what it costs a component, written out. Nothing unfair about this version — it is the shape the pattern requires:
// Path 3, the compatibility one. A component control reaches a form by
// providing an accessor and hand-wiring the callbacks the framework hands back.
@Component({
selector: 'legacy-rating',
templateUrl: './legacy-rating.html',
providers: [
{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => LegacyRating), multi: true },
],
})
export class LegacyRating implements ControlValueAccessor {
protected value = 0;
protected disabled = false;
private onChange: (value: number) => void = () => {};
private onTouched: () => void = () => {};
writeValue(value: number | null): void {
this.value = value ?? 0;
}
registerOnChange(fn: (value: number) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
protected pick(value: number): void {
this.value = value; // mutate a field…
this.onChange(value); // …then tell the framework about it, by hand
}
} Five members exist to move one number across a boundary. The state is a plain field, so the component needs a zone or a markForCheck() to repaint after writeValue; nothing type-checks that the rendered value and the written one agree; and setDisabledState is a second, separate way of being disabled that a template binding knows nothing about.
Now the same control on the native path — this is the library's own source, trimmed:
// Path 2, the native one. This is projects/lib/rating/rating.ts, trimmed to
// the members the form actually touches — every line below is in that file.
@Component({
selector: 'wr-rating',
templateUrl: './rating.html',
encapsulation: ViewEncapsulation.None,
host: { '[class]': 'classes()' },
})
export class WrRating implements FormValueControl<number | null> {
/** The rating. Bound by `[formField]`, or two-way via `[(value)]`. */
readonly value = model<number | null>(null);
/** Emitted on blur so a bound field can mark itself touched. */
readonly touch = output<void>();
/**
* Disable interaction. Bound automatically from the field's disabled state
* when used with `[formField]`.
*
* @default false
*/
readonly disabled = input(false, { transform: coerceBooleanProperty });
/** Transient hover preview — overrides `value` for display when set. */
protected readonly hoverValue = signal<number | null>(null);
private commit(value: number | null): void {
this.value.set(value);
this.hoverValue.set(null);
}
} Three members carry the whole contract, and all three are ordinary component API — the other two below are the component's own display state, which the form never sees. value is a model(), so [(value)]="score" works with no form anywhere in the picture. disabled is an input(), so a template can set it and a field can set it and the component never learns which. Nothing is registered, nothing is forwarded, and the framework reads a signal instead of being told.
Running, on this page. The rating and the checkbox below are bound to two fields of one form() — the rating writes its value model, the checkbox its checked:
<wr-rating [formField]="demo.score" ariaLabel="How likely are you to recommend ngwr?" />
<wr-checkbox [formField]="demo.contact">You may follow up by email</wr-checkbox>
<p>The model, live: {{ demoJson() }}</p> The classic bindings still work, and not through a fallback path inside the library. Angular 22 synthesises the accessor for a signal-forms control, so reactive and template-driven forms reach the same value model:
<!-- Still supported, and not a fallback path inside the library: Angular 22
synthesises the accessor for a signal-forms control, so the classic
bindings reach the same `value` model the field would have written. -->
<wr-rating [(ngModel)]="score" />
<wr-rating [formControl]="scoreControl" /> With the caveat that belongs next to it: that synthesis is Angular's, and Angular tests it — ngwr's own suite barely does. [(ngModel)] is bound to a real ngwr control in one spec, and no spec binds [formControl] to one at all. It is the last uncovered corner named on the Quality page, and it is here because this paragraph is the one you would lean on hardest.
Why the incumbents cannot simply drop it
The honest reason this window exists — and roughly when it closes.
A ControlValueAccessor is not an implementation detail a library can delete. It is the public contract every consumer's reactive form is already bound through, so removing one is a breaking change for every app on the library. A project with two million weekly downloads drops it slowly, behind a major, or keeps both forever. A project with 151 drops it in an afternoon.
That is the whole shape of the advantage: it exists because ngwr has almost nothing to break, and it closes as the incumbents ship the native contract alongside their accessors. Taiga UI is closest and deserves the credit — it already ships a structurally identical interface of its own, and its source carries a note to switch to Angular's once v22 becomes its floor. NG-ZORRO started in 22.0.1, with one control. Treat the forms row in the table below as a snapshot, not a moat.
Zoneless, and since when
The second half of the same argument, and the one that is countable rather than arguable — including the release it became true in.
# Everything the "modernity" claim rests on, as greps over projects/lib.
grep -rn '@NgModule' projects/lib --include='*.ts' | wc -l # 0
grep -rn 'standalone: true' projects/lib --include='*.ts' | wc -l # 0
grep -rn 'ChangeDetectionStrategy.OnPush' projects/lib --include='*.ts' | wc -l # 2
grep -rn 'ControlValueAccessor' projects/lib --include='*.ts' | wc -l # 16
# The last two need their answer read rather than counted. The two OnPush
# declarations are legacy files under window/; the sixteen mentions of
# ControlValueAccessor are all comments saying there is not one — fifteen in a
# component's own JSDoc, one in a spec's.
# And since "always" is the claim people check first — it is not the claim.
# The library was rebuilt at v7 and the two zeroes above date from there:
git grep -l '@NgModule' v6.0.0 -- projects/lib | wc -l # 1
git grep -l '@NgModule' v7.0.0 -- projects/lib | wc -l # 0
git show v6.0.0:package.json | grep zone.js # ~0.15.0
git show v12.0.0:package.json | grep zone.js # (nothing)Not from the first commit, and the repository will tell you so. ngwr started in August 2022 on Angular 14, with zone.js in its dependencies and sixteen @NgModule declarations in the library at v5.0.3 — v6 was down to one, dialog/dialog.module.ts. The zeroes above date from the v7 rebuild, which is what that release was: signals throughout, the last module deleted. The zoneless bootstrap arrived a release earlier, in v6.1.0, and in the showcase before the library. What ships today has no zone.js code path to keep alive and no module graph to unwind, and that is the claim — a property of the package, not a biography.
What it buys is narrow and worth saying plainly: an app already running provideZonelessChangeDetection() does not have to keep Zone.js loaded for its UI kit, and nothing in the library asks Angular to check a component that did not change. It does not make anything faster on its own.
Where ngwr sits
Nine factors that change a decision, across the five libraries a team in this position actually shortlists.
Deciding factor | ngwr 12 | Angular Material 22 | PrimeNG 22 | NG-ZORRO 22 | Taiga UI 5 |
|---|---|---|---|---|---|
| Signal Forms binding | Native. Nineteen value controls implement FormValueControl or FormCheckboxControl; no ControlValueAccessor in the package. One public component with a value model implements neither: [wrColorPickerTrigger]. | ControlValueAccessor. Eleven controls declare one; no FormValueControl in the tarball. | No FormValueControl in the shipped .d.ts. | Started. 22.0.1 added Signal Forms state to the input; 41 files still reference ControlValueAccessor. | A structurally identical interface of its own, with a source note to adopt the Angular one once v22 is its floor. |
| License | MIT. | MIT. | Not MIT from 22.0.0; MIT through 21.1.9. Read 2026-08-20 — the vendor states the current terms. | MIT. | Apache-2.0. |
| Weekly npm downloads | 151. | 2,015,398. | 644,037. | 225,137. | 19,915. |
| GitHub stars | 3. | 25,035. | 12,495. | 9,167. | 4,041. |
| Who maintains it | One person — 1,409 of 1,480 commits, under four author identities. The next human contributor has 13; the two bots have 29 each. | The Angular team at Google. | PrimeTek, commercially. | Over a hundred contributors. | Over a hundred contributors, backed by T-Bank. |
| CDK test harnesses | 103 classes across 70 entry points. | 97 classes. The CDK harness pattern started here; ngwr copied it. | None found in the tarball. | None found in the tarball. | None found in the tarball. |
| Locales in the box | Two — English and Russian. | Defers to Angular i18n and MAT_DATE_LOCALE. | Not measured. | Many more than two; the exact count was not measured. | 23 language packages. |
| Angular peer range | >=22.0.0, with no upper bound — permissive, and a promise about nothing. | ^22 || ^23. The only one already declaring v23. | ^22.1. | ^22. | >=19. The widest back-compat of the five. |
| Runtime dependencies | One — tslib. @angular/cdk is a required peer, so parse5 arrives with it. | One — tslib. The CDK adds parse5, exactly as it does for ngwr. | Seven, including the license manager. | Five. | One — tslib, plus fifteen peers. |
Downloads, stars, versions and license metadata were read from the npm registry and the GitHub API on 2026-08-20; the forms, dependency and harness rows come from the published tarball of each library's then-current version. They move, and a competitor's licensing terms are the vendor's to state — check them there before you rely on a row here. The ngwr column is measured in this repository and reproducible: find projects/lib -name ng-package.json for the entry points, a grep for ControlValueAccessor, and git shortlog -sn --all for the commits. That last one prints seven rows, not one: the maintainer is four of them (977 + 422 + 8 + 2 = 1,409, an identity per machine and per email over four years), two are bots at 29 each, and one is the other human at 13.
Three more libraries were looked at and left out of the table rather than padded into it. Spartan/ng is a different product shape — its helm layer copies component source into your repository, which is maximum customisation and maximum maintenance. Clarity is alive and shipping but reports conflicting license metadata (NOASSERTION on GitHub, MIT on npm), which is a question for your legal review rather than a row here. Nebular's last release was 17.0.0 in January 2026, on an Angular 21 peer, with no Angular 22 support yet.
Accessibility, including the part that fails
Angular Material is the credible leader here and this page is not going to argue otherwise.
Material has the longest APG track record in the ecosystem and is the reference implementation most auditors already know. What ngwr does differently is not claim more, it is gate: the structural axe baseline is literally {}, so any serious or critical violation across the prerendered site is a red build rather than a backlog item, and two more browser sweeps run nightly — painted contrast in both themes, and the full axe rule set inside opened overlays, hovers and focus rings, which prerendered HTML cannot reach.
Those two nightly baselines are not empty, and pretending otherwise is the failure mode here. Four accepted entries in the contrast baseline and ten in the state one. Behind them: out-of-month calendar days at 2.23:1, the carousel's 8×8 pagination dots, the window chrome's 14×14 traffic-light buttons, the event calendar's 20px chips, a token-swatch grid that labels every shade of a ramp with a token calibrated for the base shade, and disabled controls axe cannot recognise as disabled. Each carries the alternative, costed, in the baseline file — and each is a WCAG failure that is on the site today. The Quality page lists them and what every gate cannot see.
And the disclosure that has to travel with that claim. --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. Two fixes were costed — darkening the one token, or splitting out a separate control-border token — and both were rejected on how they look. The hairline stayed. No gate reports this, because axe ships no non-text contrast rule; it is written down here so an auditor finds it in the docs rather than in the product.
Choose something else if…
Named competitor, named situation. If none of these fit, the case above is the case.
- Angular Material — you need the safest institutional bet. Maintained by the Angular team, two million weekly downloads, already declaring an Angular 23 peer, the deepest accessibility record, and 97 test harnesses. Its cost is the narrowest catalog of the majors: no data grid with pinning, grouping and tree rows, no transfer, no cascader, and controls that still bind through
ControlValueAccessor. - PrimeNG — you need the widest catalog in Angular (rich-text editor, organisation chart, tree table, galleria, Chart.js integration) and a vendor you can pay. Read the vendor's current terms rather than this page: the npm
licensefield changed at 22.0.0, and the terms are the vendor's to state, not ours to summarise. Everything through 21.1.9 is MIT. - NG-ZORRO — you are building Ant Design, or you ship to a broad international audience. Its bundled locale set is far larger than ngwr will have for a long time.
- Taiga UI — you need to support Angular back to v19 while still moving toward signals, or you want the largest genuinely open-source catalog with institutional backing. It is also the library closest to ngwr's forms position and has committed publicly to the same destination.
- Spartan/ng — you are a Tailwind v4 shop and want shadcn-style ownership, with component source copied into your repository and styled however you like. You take on maintaining that copy.
- Clarity — you are in a Broadcom/VMware environment or need that specific design language.
- Nebular — you are already on it. Last release 17.0.0 in January 2026, roughly two releases a year, no Angular 22 support yet. This is not a reason to start.
What it costs to adopt, and how to leave
The last question is the one nobody asks until they need the answer.
Getting in. One runtime dependency, tslib. Six required peers, four of which any Angular app already has — @angular/core, common, forms, platform-browser — plus rxjs and @angular/cdk, which is not in a default ng new app and brings parse5 with it. @angular/router, date-fns, luxon and lucide are declared optional and stay uninstalled unless you use the features that need them. ng add ngwr wires the rest.
The surface you are coupling to. Wider than the TypeScript. Components are ViewEncapsulation.None, so the BEM .wr-* classes are public API and are treated as such; the --wr-* custom properties are public too, and theming is a runtime CSS-variable layer rather than a Sass compile step, which is what makes a per-tenant palette possible without a rebuild.
Getting out. MIT, source on GitHub, nothing compiled-only, no license key, no telemetry, no registration. Because the controls are signal-forms controls, leaving means swapping components, not rewriting forms: a [formField] bound to <wr-select> binds the same way to a native <select> or to any other control that implements the same contract. The one thing that does not come back for free is the styling you wrote against the .wr-* classes.
Moving between majors.ng update ngwr@N, with one migration per major that can honestly be codemodded — v12 rewrites the date-adapter import paths, and its other breaking change is a return type that changed, which the compiler names at every call site. v10 and v11 ship no migration on purpose: their breaking changes were painted colour, and an empty codemod would tell you your visual regressions were handled when they were not. The migration guide lists all of it.
Bundle figures, since they usually come up next: there are none here. Nothing in this build measures them, so anything quoted would be a number somebody typed once — which is the failure this page is written against, and it is worse in a size comparison than anywhere else. What is structural and checkable instead: the package ships one FESM bundle per entry point, so an app pays for what it imports and nothing else, and there is no apples-to-apples third-party benchmark across these five libraries either. Measure the ones you import, in your own build, against the alternative you are actually considering.