The short version
ngwr is a signals-first Angular UI library written for one version of Angular and one way of writing forms. Twenty 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 229 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, competitor by competitor.
The numbers that belong before the argument rather than after it, so a reader can leave here. The bus factor is one: more than nine commits in ten are by one person under four author identities, the only other human contributor has 13, and the rest are two bots, from the first commit on 2022-08-22 — four years, one pair of hands. There is no vendor — no paid support, no SLA, no consulting arm, nobody to escalate to; an issue tracker is the whole of it. Majors move fast: eight since 2026-06-12, v7 through v14, five of them carrying an ng update codemod and three deliberately not. 22 locale catalogs ship in the box.And almost nobody uses it — ~150 weekly npm downloads and 3 GitHub stars, read on 2026-08-20. 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.
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. No ControlValueAccessor is created: Angular's NgModel / FormControlDirective drive the control's value or checked model directly, so reactive and template-driven forms reach the same model:
<!-- Still supported, and not a fallback path inside the library: no
ControlValueAccessor is created. Angular's NgModel / FormControlDirective
drive the same `value` model the field would have written. Template
validator directives (required, minlength, …) are NOT applied on this
path — put validators on the FormControl. -->
<wr-rating [(ngModel)]="score" />
<wr-rating [formControl]="scoreControl" /> With the caveats that belong next to it. That bridge does not apply template validator directives: a required or minlength on an ngwr control produces no error and the form submits, so validators go on the FormControl. It also ignores updateOn, and a { emitEvent: false } write needs markForCheck() — the forms guide has each one. The bridge is Angular's, and ngwr's own suite binds only some of the value controls through it; the Quality page names which. These caveats sit 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 ~150 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' \
| grep -vE ':[0-9]+:\s*\*' | wc -l # 0
# Two of those need a word. The two OnPush declarations are legacy files under
# window/. And the second grep on the last one drops comment lines: every
# mention of ControlValueAccessor in the library is a comment saying there is
# not one, so leave that filter off to read them.
# 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.
The reverse direction is supported, and it is not an accident of the design. An app on provideZoneChangeDetection() can install ngwr as it is: every component reads signals, which are change-detected the same way under either scheduler, and the library registers its hot listeners — scroll, pointer move, document click, the animation loops — inside NgZone.runOutsideAngular() in nine files, so a zone app does not pay a change-detection cycle per frame for a spotlight gradient or a scroll-driven back-to-top button. OnPush hosts are fine for the same reason: a signal read in a template marks its own consumer dirty and does not depend on the host's strategy. What ngwr will not do is require Zone.js — the library's own suite and the showcase both run zoneless, so that is the configuration under continuous test, and a zone-specific regression would reach you before it reached us.
Where ngwr sits
Nine factors that change a decision, across the five libraries a team in this position actually shortlists.
| Deciding factor | ngwr 14 | Angular Material 22 | PrimeNG 22 | NG-ZORRO 22 | Taiga UI 5 |
|---|---|---|---|---|---|
| Signal Forms binding | Native. Twenty 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 read 2026-08-20 | ~150. | ~2M. | ~640k. | ~230k. | ~20k. |
| GitHub stars read 2026-08-20 | 3. | ~25k. | ~12k. | ~9.2k. | ~4k. |
| Who maintains it | One person — more than nine commits in ten, under four author identities. The only other human contributor has 13; the rest are two bots. | The Angular team at Google. | PrimeTek, commercially. | Over a hundred contributors. | Over a hundred contributors, backed by T-Bank. |
| CDK test harnesses | 106 classes across 72 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 | 22 catalogs, gated against the English key set. | Defers to Angular i18n and MAT_DATE_LOCALE. | Not measured. | Considerably more; 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. The download and star rows are rounded to two significant figures and dated in the table itself: those two move every day, so an exact figure read once is false within a week while still reading as a measurement, where ~2M stays true for months and cannot be mistaken for one. ngwr's own commits are given as a share rather than as totals, because the maintainer's total moves with every commit; the command below prints the exact figures. The rest move too, 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 (an identity per machine and per email over four years), two are bots, and one is the other human.
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. Behind the accepted entries in both: 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 table header's 12×12 sort and filter controls on a fine pointer, 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 its reason in the baseline file, and every one but the disabled controls, which WCAG exempts, is a 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, or you need to hire people who already know the library. Maintained by the Angular team, two million weekly downloads, already declaring an Angular 23 peer, the deepest accessibility record, and 97 test harnesses. Against that, ngwr has ~150 weekly downloads and 3 GitHub stars, read on 2026-08-20, and nobody's CV says ngwr. Material's own 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) or a vendor you can pay. ngwr has both of the first two, narrower:
wr-editoris a ProseMirror editor with a fixed schema and no tool that inserts a table or an image, andwr-graphdraws an organisation chart that is display-only. It has a tree table as well:wr-table'schildrenKeymakesitemsthe roots and the table announces atreegrid. And.xlsxexport is a standing refusal rather than a gap — it needs a third-party dependency, andtslibis the only one this package has. On the vendor half, PrimeNG and Nebular sell paid support and an escalation path; ngwr has none of that. Read the vendor's current terms rather than this page: the npmlicensefield 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 in a language ngwr does not. 22 catalogs are in the box now and every one is held to the English key set by a test, but NG-ZORRO's bundled set is several times larger and will stay that way. Check the list first: if your language is on it you have nothing to write, and if it is not,
provideWrI18n()takes your own and gates it the same way. - Taiga UI — you need to support Angular back to v19 while still moving toward signals, you want the largest genuinely open-source catalog with institutional backing, or you ship in more than two languages and want the packs written for you: it publishes 23 of them. 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 the prosemirror-* packages behind wr-editor are declared optional and stay uninstalled unless you use the features that need them. No icon set is a peer at all. 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. One ng update ngwr to the latest major, from whichever one you are on — it runs every migration in between, in order. There is one migration per major that can honestly be codemodded — v14 rewrites six renames and reports the three changes that need a decision instead of a substitution; v12 rewrote the date-adapter import paths, and its other breaking change was 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. v13's migration is the other shape: it runs, it FINDS every host-id selector on <wr-checkbox> / <wr-radio> / <wr-switch> — the id moved to the inner <input>, where <label for> can reach it — and rewrites none of them, because whether wr-checkbox#agree should become #agree, wr-checkbox:has(#agree) or a class is a decision only its author can make, and a selector that stops matching is silent in CSS, in querySelector and in a test locator alike. The migration guide lists all of it.
How often that happens, and what the version number is worth. Eight majors shipped between 2026-06-12 and 2026-09-04 — the honest number, and the reason the row above calls the peer range a promise about nothing. Two things bound it. A breaking change can no longer arrive in a minor or a patch: the release script refuses that bump when any commit since the last tag is marked breaking, which is what 12.2.0 — a minor with a BREAKING CHANGES heading — did not have. And the support window is written down rather than implied: the current major in full, the one before it for mechanical security fixes only, and that row closes when the next major ships. Versioning & support has the rule, the table and the commands to check both against the package rather than against this page.
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 — the installation page carries the two commands that answer it, plus the one figure that is a property of the package rather than of your app: the umbrella stylesheet, which is every component's CSS whether you render it or not. Two things worth knowing before you measure. An entry point's static imports come with it whatever the template turns on, so a bare <wr-table> still bundles the checkbox, the pager and CDK's drag-drop; and Angular's own “estimated transfer size” runs 10–20% under gzip -9, so gzip the file yourself if you are checking against a budget.
See also
- GuideQualityThe gates behind the accessibility and testing rows, in numbers.
- GuidePlaygroundTry the claim in a real Angular 22 app before installing anything.
- GuideMigrationWhat each major broke, and which of them ship a codemod.
- GuideVersioning & supportThe release cadence, the semver rule that is enforced, and how long a major is patched.