NGWR is an Angular UI library that binds straight to Signal Forms. Nineteen
value controls implement FormValueControl / FormCheckboxControl themselves,
so [formField] writes the component's own value / checked model — there
is not one ControlValueAccessor in the library. Zoneless by construction, not
zoneless-compatible: signal inputs, signal state, afterNextRender() for DOM
work, and no @NgModule or @Input() decorator anywhere in the source. 225
tree-shakable entry points, on @angular/cdk for overlay, portal and a11y
primitives.
Try it in the browser — no install. How it compares — what the other Angular UI libraries do about Signal Forms today, counted rather than asserted. Docs and live demos.
import { Component, signal } from '@angular/core';
import { FormField, email, form, required } from '@angular/forms/signals';
import { WrCheckbox } from 'ngwr/checkbox';
import { WrFormField } from 'ngwr/form';
import { WrInput } from 'ngwr/input';
@Component({
selector: 'signup-card',
imports: [FormField, WrCheckbox, WrFormField, WrInput],
template: `
<wr-form-field label="Work email" required>
<input wrInput type="email" [formField]="signup.email" />
</wr-form-field>
<wr-checkbox [formField]="signup.agree">I agree to the terms</wr-checkbox>
`,
})
export class SignupCard {
private readonly model = signal({ email: '', agree: false });
// `[formField]` binds to the control's own `value` / `checked` model, and
// `<wr-form-field>` resolves the error copy from the i18n catalog — so
// neither an accessor nor a `<wr-form-error>` has to be written by hand.
readonly signup = form(this.model, path => {
required(path.email);
email(path.email);
});
}Classic [(ngModel)] and reactive forms still work — Angular 22 synthesises the
accessor for a signal-forms control — and every control is usable standalone
through its two-way [(value)] / [(checked)] model.
Status: active development. v14 is the current major line (Angular 22 peer). Upgrading?
ng update ngwr@14rewrites v14's six renames and reports the changes no codemod should guess at — the migration guide walks every step. Open an issue if something breaks or feels wrong.
| Peer | Range |
|---|---|
@angular/core |
>= 22.0.0 |
@angular/common |
>= 22.0.0 |
@angular/forms |
>= 22.0.0 |
@angular/cdk |
>= 22.0.0 |
@angular/platform-browser |
>= 22.0.0 |
@angular/router (optional) |
>= 22.0.0 |
rxjs |
^7.0.0 |
date-fns (optional) |
^3.0.0 || ^4.0.0 |
luxon (optional) |
^3.0.0 |
lucide (optional) |
>= 1.0.0 |
TypeScript ~6.0.x (Angular 22's compiler declares typescript >=6.0 <6.1) and
a Node version Angular 22 accepts — ^22.22.3 || ^24.15.0 || >=26. ngwr itself
declares neither: no engines field and no TypeScript peer, because it ships
pre-compiled bundles and .d.ts files, so the versions that bind are the ones
your Angular names. Contributing to this repo needs the narrower
^24.16.0 || >=26 it pins (.nvmrc says 24), plus pnpm ≥ 11.10.
The floor is real; the missing ceiling promises nothing. ngwr ships
partially compiled, and the floor is enforced by the bundles rather than by the
range — but by a minority of them, which is why the failure is confusing when it
arrives. Of 654 declarations, 25 record minVersion: "22.0.0" (the service
declarations, across 22 files); the rest are older shapes an old linker reads
fine. So the install succeeds — every package manager treats an unmet peer as a
warning — and ng build then dies inside one fesm2022 bundle with a message
that names no version at all. Read the peer warning at install time. Above the
floor, an open-ended range only means your package manager will not stop you
installing next to an Angular this release was never built against.
grep -ho 'minVersion: *"[^"]*"' node_modules/ngwr/fesm2022/*.mjs | sort | uniq -cSemver, with the one rule that matters made mechanical: a breaking change
cannot ride a minor or a patch. release:prepare refuses --bump=minor and
--bump=patch when any commit since the last release tag carries a ! type or
a BREAKING CHANGE: footer — it prints the offending subjects and exits
non-zero, and since a release is cut only by that workflow, the refusal is the
release. It exists because the opposite shipped: 12.2.0 was a minor carrying a
BREAKING CHANGES section, and ^12.1.0 picked it up silently. So a caret
range on a major is a safe range now. It was not then. What the guard cannot see
is a break nobody labelled as one — it reads commit metadata, which is what
commitlint on every commit and PR title gives it to read.
Public API is three surfaces, and all three move only in a major: the exported
TypeScript, the .wr-* BEM class names (components ship
ViewEncapsulation.None), and the --wr-* custom properties. Anything marked
@internal is not API, and the marker survives into the shipped types —
grep -rn "@internal" node_modules/ngwr/types is the whole check. A few helpers
are exported, unmarked and undocumented; treat those as unsupported until a page
describes them, and open an issue naming the one you need.
Two lines are supported at a time: the current major in full, the one before it for mechanical security fixes only, and that second row ends when the next major ships. Read it against the cadence rather than a calendar — eight majors shipped between 2026-06-12 (v7.0.0) and 2026-09-04 (v14.0.0). Table, targets and the private reporting route: SECURITY.md. Full policy, with the commands to check every claim in it against the installed package: https://ngwr.dev/start/versioning.
The schematic does the whole Install + Styles section for you — it installs
ngwr and its peers, appends @use 'ngwr'; to your global stylesheet, and
prints a provider snippet tailored to your answers (date adapter, density,
theme) to paste into bootstrap:
ng add ngwrOr wire it up by hand:
pnpm add ngwr @angular/cdk
# or
npm install ngwr @angular/cdk
# or
yarn add ngwr @angular/cdkBeyond Angular itself, @angular/cdk and @angular/forms are the required
peers — forms because the value controls implement its Signal Forms interfaces.
@angular/router is optional, and since v14 nothing pulls it in unless you ask:
a <wr-tab routerLink> needs WrTabsRouting from ngwr/tabs/router on the
strip, and <wr-loading-bar> follows navigation only once you add
provideWrLoadingBarRouter() from ngwr/loading-bar/router. Both used to be
automatic, which cost every app 66–76 kB of router whether it routed or not.
A stock ng new app already ships forms and router, so
@angular/cdk is the only one you have to add — which is why it is on the
install lines above. Add an icon set and a date library only if you use them —
lucide (or feather-icons) for the icon adapters, and date-fns or luxon
for the calendar / date-picker, which otherwise runs on a built-in native
Date adapter. The Quick start below registers a lucide icon, so it needs
lucide:
pnpm add lucideThe fastest way — pull in everything (theme tokens + all component styles):
// styles.scss
@use 'ngwr';Good for a spike, but it is every entry point at once — about 265 kB of CSS
(~40 kB over the wire), which is over half the 500 kB initial budget a fresh
ng new warns at before any of your own code. For anything you intend to keep,
opt in per component below and the sheet stays proportional to what you actually
render.
Prefer to opt in per-component? Each component has its own SCSS entry that pulls in the theme automatically:
@use 'ngwr/theme'; // CSS custom properties (--wr-color-*, --wr-font-*, etc.)
@use 'ngwr/button';
@use 'ngwr/input';
@use 'ngwr/checkbox';Opt-in utilities (not part of @use 'ngwr'):
@use 'ngwr/reset'; // box-sizing, body margin, sane defaults
@use 'ngwr/grid'; // .grid, .container, .col-*
@use 'ngwr/animations'; // .wr-animate-fade-in, .wr-animate-slide-up, …
@use 'ngwr/typography-utilities'; // .wr-text-*, .wr-font-* utility classes
@use 'ngwr/breakpoints' as bp; // SCSS mixins only, no CSS outputngwr/typography-utilities is the utility-class sheet — not ngwr/typography,
which is the larger wrTypography component entry.
Components render with ViewEncapsulation.None, so their .wr-* BEM classes and
their --wr-* custom properties are reachable — and both are treated as public
API: renaming one is a breaking change carrying a migration note. The DOM shape
is not. Which element holds which class, how deeply they nest, whether a wrapper
exists — all of that may move in a minor. A rule keyed on one class survives a
rename; a descendant selector that encodes a structure
(.wr-select__trigger > span > svg) does not. And a surviving rule is not a
winning one: 27 components ship their own stylesheet, which Angular emits after
your linked styles.css, so a single-class override loses to it at equal
specificity — reach for their --wr-* token instead. See
Theming for the full statement and the list.
// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideWrOverlay } from 'ngwr/overlay';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, {
providers: [
provideWrOverlay(), // isolated overlay container
],
});// app.component.ts
import { Component, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { Check } from 'lucide';
import { WrButton } from 'ngwr/button';
import { provideWrIcons } from 'ngwr/icon';
import { lucideIcons } from 'ngwr/icon/adapters/lucide';
import { WrInput } from 'ngwr/input';
@Component({
selector: 'app-root',
imports: [FormsModule, WrButton, WrInput],
providers: [provideWrIcons(lucideIcons({ checkmark: Check }))], // tree-shaken icons
template: `
<input wrInput [(ngModel)]="name" placeholder="Your name" />
<button wr-btn color="primary" icon="checkmark" (click)="greet()">Hello</button>
`,
})
export class AppComponent {
readonly name = signal('');
greet(): void {
console.log('Hi', this.name());
}
}Value controls are Signal Forms-native, so [formField] binds straight
through — no ControlValueAccessor anywhere in the chain:
// profile-form.ts
import { Component, signal } from '@angular/core';
import { FormField, form } from '@angular/forms/signals';
import { WrCheckbox } from 'ngwr/checkbox';
import { WrInput } from 'ngwr/input';
@Component({
selector: 'app-profile-form',
imports: [FormField, WrCheckbox, WrInput],
template: `
<input wrInput [formField]="profile.name" placeholder="Your name" />
<wr-checkbox [formField]="profile.agree">I agree</wr-checkbox>
`,
})
export class ProfileForm {
readonly model = signal({ name: '', agree: false });
readonly profile = form(this.model); // FieldTree — profile.name, profile.agree
}Browse the full catalog with live demos at ngwr.dev. Each entry below is a tree-shakable subpath —
import { … } from 'ngwr/<name>'. A few share a package:form-fieldships fromngwr/form,button-groupfromngwr/button, andqris the subpath behind theqrcodedocs page.
Form — calendar, cascader, checkbox, color-picker, date-picker, file-upload, form, form-field, input, input-number, input-otp, knob, mention, radio, rating, schema-form, segmented, select, slider, switch, textarea, transfer.
Buttons — button, button-group, speed-dial.
Data — drag-drop, event-calendar, pagination, pull-to-refresh, table, tree, virtual-scroll.
Feedback — alert, empty, progress, result, skeleton, spinner.
Display — avatar, badge (incl. wr-tag), compare, counter, descriptions, divider, image-cropper, keyboard, lightbox, markdown, qr, statistic, timeline.
Layout — card, carousel, collapse, layout, list, page-header, splitter, toolbar.
Navigation — anchor, back-top, breadcrumbs, burger, dropdown, sidebar, stepper, tabs.
Overlays — action-sheet, command-palette, context-menu, dialog, drawer, popconfirm, popover, toast, window.
Charts — bar-chart, calendar-heatmap, donut-chart, gauge, line-chart, meter-group, sparkline.
Plus icon, the experimental squircle, and the typography directive.
Animated UI effects. Mix of in-house components + ports of reactbits.dev — each port carries a credit chip on its docs page. Defaults are theme-aware (light + dark), and every component in this section honors prefers-reduced-motion — the one exception is spotlight-card, whose highlight only tracks the pointer. The scope of that sentence is the thing to read carefully: it says nothing about the rest of the catalog, and until v13 it was inviting a reader to over-generalise. The always-on chrome — the spinner and skeleton that animate on essentially every page, plus the enter animations on dialog, drawer, dropdown, popconfirm, toast, lightbox and the responsive bottom sheet — had no guard at all. It has one now, in the theme layer (theme/styles/_motion.scss) rather than per component, so the set is reviewable in one place.
aurora, blur-text, border-glow, circular-text, click-spark, confetti, decrypt-text, falling-text, fuzzy-text, glitch-text, gradient-text, marquee, rotating-text, shiny-text, splash-cursor, split-text, spotlight-card, star-border, tilt-card, typewriter, waves.
Card packages bundle their related directives: ngwr/spotlight-card exports WrSpotlight; ngwr/tilt-card exports WrTilt; ngwr/shiny-text exports WrShimmer.
autofocus, autosize, click-outside, copy-to-clipboard. affix ships as its own entry (ngwr/affix).
wrBytes, wrDate, wrMark, wrNumber, wrPlural, wrRange, wrTruncate.
clipboard, cookie, density, hotkey, loading-bar, media, meta, platform, scroll, storage, theme, tour, i18n.
Bundled ValidatorFns composing cleanly with Angular's built-in Validators: cardNumber (Luhn), cvc, hexColor, iban (mod-97), match (sibling control), matchFields (group-level), maxDate, minDate, noWhitespace, oneOf, url. See docs.
Math (clamp, round), coercion (numAttr), css helpers (resolveCssSize, getRootFontSize), ids (randomId), type guards (isDefined, isNonEmptyArray, isObservable), keyboard helpers (KEYS, hasModifier, isPrintableKey), functional primitives (noop, badgeLog, debounce, throttle), focus management (getFocusableElements, trapFocus). See docs for the full list. Shared shapes (Maybe, SafeAny, WrColor, …) are documented under Interfaces.
- Color — design tokens and palette.
- Grid — opt-in 12-column layout.
- Overlay — isolated CDK overlay container,
provideWrOverlay(). - Mobile & responsive — responsive overlays, touch targets & density, swipe gestures, safe-area insets, container-query layouts.
- Typography —
wrTypographydirective: headings, paragraphs, lists, links, code. - Icons —
ngwr/iconregistry. UsesvgIcon()for any set that ships raw SVG files (Tabler, Phosphor, Heroicons, Iconoir, Radix, Bootstrap, or your designer's own), plus thin adapters for Lucide (ngwr/icon/adapters/lucide) and Feather (ngwr/icon/adapters/feather), whose packages don't ship SVGs. - Date adapters —
ngwr/date(nativeDate, no extra package),ngwr/date/adapters/fns,ngwr/date/adapters/luxon. Wire one withprovideWrDateAdapter()— plus{ adapter: WrDateFnsAdapter }/{ adapter: WrLuxonAdapter }for the library-backed ones — to power calendar + every mode of date-picker. - Component defaults —
ngwr/config.provideWrConfig({ button: { size: 'sm' } })sets what a component falls back to when a template says nothing; a bound value always wins, and a boundfalsebeats a configuredtrue, so a config never has to be escaped. Reference.
- Standalone & signals-first. Every component is standalone and uses
input()/model()/output()/signal()/computed(). Zoneless-ready. - Signal Forms native. Nineteen value controls implement
FormValueControl/FormCheckboxControl, so[formField]binds straight through — there is noControlValueAccessorin the library at all.[(ngModel)]and reactive forms keep working through Angular's bridge, and every control also works standalone via[(value)]/[(checked)]. - CDK-powered. Overlays, portals, and a11y come from
@angular/cdk. We addprovideWrOverlay()so NGWR overlays never collide with other CDK consumers (Material, NG-ZORRO, etc.). - Mobile & responsive. Overlays collapse to bottom-sheets on small screens (
provideWrResponsiveOverlays()), touch targets grow to ≥44px on coarse pointers, atouchdensity preset enlarges the nine control families that read the multipliers, and drawer / lightbox / toast / carousel respond to swipe gestures. Fixed surfaces respectenv(safe-area-inset-*), and layout components (descriptions,stepper,page-header,toolbar,pagination,table) reflow to their container via container queries. Guide. - Table, batteries included.
wr-tablecovers column pinning / resizing / drag-reorder, row selection, expandable rows, grouping, tree rows (childrenKey— the forest flattens into the same<tbody>, so pinning and cell templates keep working at every depth, and the table announces atreegrid), summary rows, CSV export (exportCsv(), dependency-free RFC 4180) and a virtualized body — all opt-in inputs on the one component. Excel (.xlsx) export is deliberately not shipped: it would mean a third-party dependency. - Tree-shakable. 225 separate ng-packagr entry points — import only what you use. Per-component FESM bundles are small: a median of ~4 KB gzipped, the heaviest (
ngwr/markdown) ~23 KB. Every runtime bundle together gzips to ~690 KB — the 70ngwr/<name>/testingharnesses aside, since they never reach an app bundle — but real apps pull a handful of entries. The only runtime dependency istslib. - Modular SCSS. Component styles are scoped through CSS custom properties. Theme tokens live in
ngwr/theme; utilities (grid,reset) and the breakpoints SCSS API are opt-in. - Tree-shaken icons.
provideWrIcons(lucideIcons({ plus: Plus }))registers only the icons you actually import. Dev-mode validation warns about unregistered icons. - Reactbits ports, dependency-free. All animation ports are reimplemented with vanilla DOM + Web Animations API /
IntersectionObserver/requestAnimationFrame/ raw WebGL — no GSAP, nomotion/react, nomatter-js, noogl. - Motion respects the OS. Every animation component short-circuits to its final state under
prefers-reduced-motion, exceptspotlight-card, which animates nothing on its own — its highlight follows the cursor. - Legible to agents. Every docs page also serves as markdown at the same URL plus
.md— reference/components/select.md is that page's prose, code samples and API tables without the site chrome. The whole catalog is at llms-full.txt, a quick-ref at llms.txt.
The package ships ngwr-mcp, a zero-dependency MCP server that makes those files askable: search_ngwr (find an entry point by what you need), get_ngwr_component, get_ngwr_api (a class's inputs / models / outputs / methods, read out of the shipped .d.ts) and get_ngwr_setup (the install, ng g ngwr:use and provider commands — returned as text; it never runs them). It adds no second copy of the catalog: it reads only files inside its own installed package, makes no network requests, and runs no commands.
{
"mcpServers": {
"ngwr": { "command": "npx", "args": ["-y", "ngwr-mcp"] }
}
}Works in Claude Code (claude mcp add ngwr -- npx -y ngwr-mcp), Claude Desktop and Cursor. To pin it to the version in your lockfile, use "command": "node", "args": ["./node_modules/ngwr/mcp/server.js"]. Guide.
Conventional commits are enforced on PR titles. Common types: feat, fix, perf, refactor, docs, style, test, build, ci, chore, revert. Optional scope is the component or area (feat(checkbox): icon mode).
pnpm install
pnpm dev # ng serve --o (showcase)
pnpm test # ng test lib (vitest)
pnpm build:lib # ng build lib + ai assets + dist assets + i18n json + schematics + mcp server
pnpm build:showcase # ai assets + showcase build + sitemap + markdown twins
pnpm lint # ng lint + eslint scripts + stylelint + colour parity + rtl + registry + tokens- Roman Khegay — code, design
MIT — free for commercial use.