Angular components that bind straight to Signal Forms.

Zoneless, SSR-safe, tree-shakable — with no accessor layer in between.

Components

Composed in context.

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

42.1K
▲ 12.4%

+5.2K this week

Amount

Input group + prefix

$
Pay now

Notifications

Switch stack

Push notifications

Real-time activity

Daily digest

8:00 AM summary

Beta features

Early access

Notification

Alert surface

Payment received

$129.99 from Roman

2 new comments

View thread

Empty state

Empty primitive

No projects yet
Create your first project to get started.
New project

Team

Avatar stack + badge

Sofia MarinJMAKRT+4

8 collaborators

Synced

Topics

Tag cluster

AngularStandaloneSignalsSSRZonelessA11y

Tabbed view

Tabs + content

Standalone components, signals-first.

Page

Pagination

1 2 3 4 5 … 8 2 / 8

Showing page 2 of 8

Profile

Form field + hint

3–20 chars, no spaces.
Public, lowercase.

Theming

Segmented drives the page theme

Search

Input + kbd shortcut

⌘K

Press ⌘K from anywhere on the docs.

Loading

Skeleton placeholders

Profile

Avatar + meta + tags

Sofia Marin

Sofia Marin

Founding engineer

AngularLeadAsia/Tashkent
Follow

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.

Developer experience

Signal Forms, with nothing in between.

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.

  • Twenty value controls implement a Signal Forms interface — zero ControlValueAccessor in the library
  • Classic [(ngModel)] and reactive forms still work; standalone use is the two-way [(value)] / [(checked)]
  • Zoneless by construction — signal inputs, signal state, afterNextRender() for DOM work
  • 218 tree-shakable entry points — each component is its own, so only what you import ships
  • SSR-safe by construction — this site prerenders every route, and a prerender error fails the build
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 {}
}
Opt-in

Nothing loads until you ask.

A setting is a feature you pass, an add-on is an entry point you import, and what you skip never ships.

  • Features

    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(),
    );
    Configuration
  • Router

    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';
    Loading bar Tabs
  • Editor

    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-inputrules
    Editor
  • Languages

    22 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 });
    Translations
  • Icons

    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 }));
    Icon sets
  • Styles

    The theme first, then one line per component you render.

    @use 'ngwr/theme';
    @use 'ngwr/button';
    @use 'ngwr/select';
    Installation
Why ngwr

Built on the new Angular. Not ported to it.

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.

Signal Forms, natively

Twenty value controls implement a Signal Forms interface, so [formField] binds to their own model. No ControlValueAccessor anywhere.

Zoneless by construction

Zoneless in the source, not just compatible: signal inputs and state, afterNextRender() for DOM work, and no @NgModule or @Input() at all.

A11y is gated, not claimed

Three axe gates check structure, painted contrast and open overlays nightly and before every release. A serious violation is a red build.

Themed by tokens, not overrides

Color, radius, spacing and duration are --wr-* custom properties, and an undeclared color token fails lint. Re-skin one component or all.

Pay for what you import

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.

Dark mode is first-class

Plain CSS color-scheme: follows the OS without JavaScript and flips on any subtree, since every token that differs is a light-dark() pair.

Animations

Motion you can ship.

Beyond the primitives — a kit of motion components for hero copy, hovers, and the bits of personality that set a real product apart.

Premium

Shiny text

<wr-shiny-text />

System online

Glitch text

<wr-glitch-text />

Fast

Rotating text

<wr-rotating-text />

Decrypting…

Decrypt text

<wr-decrypt-text />

Reveal on view

Blur text

<wr-blur-text />

Split by character

Split text

<wr-split-text />

Typing words…

Typewriter

<wr-typewriter />

Gradient flow

Gradient text

<wr-gradient-text />

Companion

The checks behind ngwr.

ngwr's controls and package pass them. Yours can too.

ngtrue

Checks that keep an Angular library true.

  • Signal Forms conformancengtrue/testing checks a custom control's wiring and names the rule that broke.
  • Packaging checksngtrue/node holds the manifest against the build: exports, types, schematics and peer ranges.
pnpm add -D ngtrue
import { assertSignalFormsControl } from 'ngtrue/testing';

it('is a conforming Signal Forms control', async () => {
  await assertSignalFormsControl(() => TestBed.createComponent(Host));
});