Create account
Form field + input + button
Forms, data, navigation and feedback, side by side as in an app.
Every tile is live: type in a field, flip a switch, change the theme.
Create account
Form field + input + button
Active users
Counter + delta chip
+5.2K this week
Amount
Input group + prefix
Notifications
Switch stack
Push notifications
Real-time activity
Daily digest
8:00 AM summary
Beta features
Early access
Notification
Alert surface
Payment received
2 new comments
Empty state
Empty primitive
Team
Avatar stack + badge
Topics
Tag cluster
Tabbed view
Tabs + content
Standalone components, signals-first.
Page
Pagination
Showing page 2 of 8
Profile
Form field + hint
Theming
Segmented drives the page theme
Search
Input + kbd shortcut
Press ⌘K from anywhere on the docs.
Loading
Skeleton placeholders
Profile
Avatar + meta + tags
Sofia Marin
Founding engineer
Install
Drop-in component
import { WrButton } from 'ngwr/button';
@Component({
imports: [WrButton],
template: `<wr-btn color="primary">Save</wr-btn>`,
})
export class Demo {}Each entry point compiles on its own.
A value control here is not a wrapper around a native element that reports back through an accessor. It implements FormValueControl or FormCheckboxControl, so the field binds to the component's own value / checked model directly — and <wr-form-field> shows the message each rule carries, with no error markup to write.
ControlValueAccessor in the library [(ngModel)] and reactive forms still work; standalone use is the two-way [(value)] / [(checked)]afterNextRender() for DOM work import { Component, signal } from '@angular/core';
import { FormField, email, form, required } from '@angular/forms/signals';
import { WrButton } from 'ngwr/button';
import { WrCheckbox } from 'ngwr/checkbox';
import { WrFormField } from 'ngwr/form';
import { WrInput } from 'ngwr/input';
@Component({
selector: 'signup-card',
imports: [FormField, WrFormField, WrInput, WrCheckbox, WrButton],
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>
<wr-btn color="primary" block (click)="reserve()">
Reserve your spot
</wr-btn>
`,
})
export class SignupCard {
private readonly model = signal({ email: '', agree: false });
// `signup.email` writes the input's value and `signup.agree` the checkbox's
// `checked` model. No ControlValueAccessor — there is not one in the library.
// Each rule carries its own message and <wr-form-field> shows it, so no
// <wr-form-error> has to be written.
protected readonly signup = form(this.model, path => {
required(path.email, { message: 'Enter your work email.' });
email(path.email, { message: 'That is not an email address.' });
});
protected reserve(): void {}
}A setting is a feature you pass, an add-on is an entry point you import, and what you skip never ships.
Pass provideWr() the features whose defaults you change. One you skip is not bundled.
import { provideWr } from 'ngwr';
import { withDensity } from 'ngwr/density';
import { withTheme } from 'ngwr/theme';
import { withToast } from 'ngwr/toast';
provideWr(
withDensity(),
withToast({ duration: 6000 }),
withTheme(),
);An add-on, so the loading bar and the tabs work without @angular/router.
// The loading bar follows navigation
import {
provideWrLoadingBarRouter,
} from 'ngwr/loading-bar/router';
// Tabs with routerLink, on <wr-tabs wrTabsRouting>
import { WrTabsRouting } from 'ngwr/tabs/router';ProseMirror is an optional peer. Install it only if you use ngwr/editor.
pnpm add prosemirror-model prosemirror-state \
prosemirror-view prosemirror-commands \
prosemirror-keymap prosemirror-history \
prosemirror-schema-list prosemirror-inputrules22 catalogs ship, one entry point each. Import only the languages you use.
import {
provideWrI18n,
provideWrI18nBaseCatalogs,
} from 'ngwr/i18n';
import { wrDe } from 'ngwr/i18n/de';
provideWrI18n({ defaultLocale: 'de' });
provideWrI18nBaseCatalogs({ de: wrDe });Register the icons you use. No icon set is a dependency.
import { Check, X } from 'lucide';
import { lucideIcons, provideWrIcons } from 'ngwr/icon';
provideWrIcons(lucideIcons({ check: Check, x: X }));The theme first, then one line per component you render.
@use 'ngwr/theme';
@use 'ngwr/button';
@use 'ngwr/select';Nothing here carries a compatibility layer from the NgModule era. The bookkeeping that is left — focus traps, virtual scroll, density tokens, error wiring — is handled inside the primitive.
Twenty value controls implement a Signal Forms interface, so [formField] binds to their own model. No ControlValueAccessor anywhere.
Zoneless in the source, not just compatible: signal inputs and state, afterNextRender() for DOM work, and no @NgModule or @Input() at all.
Three axe gates check structure, painted contrast and open overlays nightly and before every release. A serious violation is a red build.
Color, radius, spacing and duration are --wr-* custom properties, and an undeclared color token fails lint. Re-skin one component or all.
218 entry points, each one tree-shakable on its own. What you don't import never lands in your bundle — the SCSS opts in per component too.
Plain CSS color-scheme: follows the OS without JavaScript and flips on any subtree, since every token that differs is a light-dark() pair.
Beyond the primitives — a kit of motion components for hero copy, hovers, and the bits of personality that set a real product apart.
Shiny text
<wr-shiny-text />
Glitch text
<wr-glitch-text />
Rotating text
<wr-rotating-text />
Decrypt text
<wr-decrypt-text />
Blur text
<wr-blur-text />
Split text
<wr-split-text />
Typewriter
<wr-typewriter />
Gradient text
<wr-gradient-text />
ngwr's controls and package pass them. Yours can too.
Checks that keep an Angular library true.
ngtrue/testing checks a custom control's wiring and names the rule that broke.ngtrue/node holds the manifest against the build: exports, types, schematics and peer ranges.pnpm add -D ngtrueimport { assertSignalFormsControl } from 'ngtrue/testing';
it('is a conforming Signal Forms control', async () => {
await assertSignalFormsControl(() => TestBed.createComponent(Host));
});