Getting started

Installation

Install NGWR, wire it into your Angular app, and start using components.

Requirements

Angular 22 is the floor, and it is a floor with no ceiling: @angular/core, @angular/common, @angular/forms, @angular/platform-browser and the @angular/cdk peer are all declared >=22.0.0, alongside rxjs 7. Four more peers are optional and marked as such, so nothing installs them for you until you reach for the feature: @angular/router (wr-sidebar, wr-breadcrumbs, and the two v14 opt-ins ngwr/tabs/router and ngwr/loading-bar/router — a plain wr-tabs or wr-loading-bar no longer reaches for it), lucide (the lucide icon adapter), and date-fns or luxon (their date adapters). tslib is the only real dependency.

On Angular 21 or 20 the install succeeds and the build is what fails.

npm, pnpm and yarn all treat an unsatisfied peer as a warning rather than an error, so adding ngwr to an Angular 21 project prints “Issues with peer dependencies found” and exits 0. The failure surfaces later, in the Angular linker, as “Unsupported enum value for [object Object]” pointing at a line inside one of the fesm2022 bundles — 25 of the package’s 654 partially-compiled declarations carry minVersion 22.0.0 — the service ones, across 22 files — and a linker that predates that shape cannot read them. Read the peer warning at install time: the message you get at build time never mentions a version.

The missing upper bound is not a promise, and the floor is not the only compatibility question. An open-ended range means your package manager will not stop you installing ngwr beside an Angular that did not exist when the release was cut — nothing more; and no TypeScript peer and no engines field are declared here at all, because the package ships pre-compiled bundles and the versions that matter are the ones your Angular names. Versioning & support covers all of it: what to do the day a new Angular major lands, how long each line keeps getting security fixes, and the semver rule — a breaking change can no longer ride a minor, because the release script refuses the bump.

Quick start

ng add runs the schematic that installs peer deps (including lucide), wires @use 'ngwr'; into your global styles, and prints a tailored bootstrap snippet based on the prompts you answer. It prints those providers rather than writing them — copy them into app.config.ts yourself.

# Recommended — runs the schematic that wires everything up for you.
ng add ngwr

# The prompts cover: styles mode, date adapter, density preset, theme.
# Answer "System" to the theme prompt (or pass --theme=system) — that is the
# answer that wires provideWrTheme(), and the default, "None", does not.
# See the Schematics page for the full list of flags.

Manual install

If you'd rather wire things yourself, install the package + the CDK peer. Unlike ng add, this does not pull lucide — add it when you want the lucide icon adapter.

pnpm add ngwr @angular/cdk
# or
npm install ngwr @angular/cdk
# or
yarn add ngwr @angular/cdk

# Only if you plan to use the lucide icon adapter (`ng add` installs it for you):
npm install lucide

Bootstrap

Two providers are what separate a working first run from a broken one. provideWrTheme() is the one to not skip: it writes [data-theme] on <html>, and without it the light tokens stay put — so a visitor whose OS is in dark mode reads near-black text on the browser's dark canvas. provideWrOverlay() gives ngwr's overlays their own container instead of the one every CDK consumer shares, and installs the visual-viewport watcher that lets a mobile bottom-sheet clear the on-screen keyboard; panels still open without it. The rest of the catalog's providers are optional — see Configuration.

// src/app/app.config.ts — the file `ng new` generates. Add the ngwr providers
// to the array that is already there.
//
// Angular 22 scaffolds a zoneless app, so there is no
// provideZonelessChangeDetection() in this file and none is needed.
import { type ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
import { provideRouter } from '@angular/router';

import { provideWrOverlay } from 'ngwr/overlay';
import { provideWrTheme } from 'ngwr/theme';

import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideBrowserGlobalErrorListeners(),
    provideRouter(routes),

    // ngwr ---------------------------------------------------------------
    // Isolates ngwr's overlays in their own container, and installs the
    // visual-viewport watcher mobile sheets need. Panels open without it —
    // they just share a DOM root with every other CDK consumer.
    provideWrOverlay(),
    // Writes [data-theme] on <html>. Without it the attribute is never set,
    // the light tokens stay, and a visitor whose OS is in dark mode gets
    // near-black text on the browser's dark canvas.
    provideWrTheme(),           // defaults to { defaultMode: 'auto' }
  ],
};

Every import is a subpath — the root is empty on purpose

import { WrButton } from 'ngwr' does not work, and it fails in a way an IDE will happily write for you. There is no barrel: the root entry point exports nothing (export {} in public-api.ts, an empty FESM module in the tarball), because a barrel re-exporting 204 entry points is a barrel every consumer's bundler has to prove it can shake. So the compiler answers TS2305: Module '"ngwr"' has no exported member 'WrButton', which names the symbol and not the mistake. The symbol always comes from the entry point, and the entry point is the last segment of the component's docs URL/reference/components/select is ngwr/select. The one lint rule below turns a wrong auto-import into a red squiggle instead of a build error.

// ✗ There is no barrel. TS2305 — 'ngwr' has no exported member 'WrButton'.
import { WrButton } from 'ngwr';

// ✓ Always the entry point, which is the last segment of the docs URL.
import { WrButton } from 'ngwr/button';        // /reference/components/button
import { WrSelect, WrOption } from 'ngwr/select';
import { WrTag } from 'ngwr/badge';            // <wr-tag> lives in ngwr/badge

// Make a wrong auto-import a lint error instead of a build error.
// eslint.config.ts
export default [
  {
    rules: {
      'no-restricted-imports': [
        'error',
        { paths: [{ name: 'ngwr', message: 'Import from an entry point: ngwr/button, ngwr/select, …' }] },
      ],
    },
  },
];

Global styles

Import once in your global stylesheet. The umbrella entry pulls theme tokens + every component's styles. Utilities are opt-in. The theming guide writes this as @use 'ngwr' as *; — same CSS either way; as * only drops the ngwr. prefix from the SCSS mixins the umbrella forwards, so reach for it when you want @include media-up(md) unqualified.

// styles.scss — import once, gets the full library
@use 'ngwr';

// Opt-in utilities
@use 'ngwr/grid';   // .grid, .container, .col-*
@use 'ngwr/reset';  // see "What the opt-in utilities do" below

What it costs, since ng new warns at 500 kB. Compiled and minified, the umbrella is about 290 kB of CSS, roughly 42 kB over the wire — every one of the ~120 component sheets, whether you render the component or not. The fifteen entries a typical CRUD screen needs (theme, density, button, input, form, icon, select, checkbox, table, pagination, dialog, toast, overlay, spinner, dropdown) come to about 87 kB / 13 kB gzipped; the theme token layer alone is 22 kB / 4 kB. Those are this release measured with the command below, not a budget — re-run it rather than trusting the numbers a page was written with.

# What any set of style entries actually compiles to, in your own checkout.
# Write the @use lines you are considering into a scratch file, then:
npx sass --load-path=node_modules --style=compressed check.scss check.css
wc -c < check.css          # minified
gzip -9 -c check.css | wc -c   # over the wire

One declaration in that CSS is not a --wr-*, and it is worth knowing about before you go looking for it: the token layer sets color-scheme: light on :root, and the dark block sets color-scheme: dark on the theme attribute. That is what keeps native scrollbars, <textarea> and date / colour inputs matching the theme you are painting — without it, an app running ngwr's light theme on a dark-mode machine got dark native controls on light surfaces. The consequence to know: load the stylesheet and skip provideWrTheme() and the UA canvas is pinned light, because nothing ever writes the attribute the dark block keys on.

Per-component styles

Each component also ships a standalone SCSS entry, and the theme layer comes with it — @use 'ngwr/button' emits the tokens too, deduped across the compilation, so you never load ngwr/theme by hand. What does NOT come with it is every other component this one renders, and that is the whole hazard of opting in per component: nothing warns you, the control just renders wrong. A missing ngwr/icon is the loudest of them: an inline SVG with no .wr-icon__svg rule falls back to the browser's default replaced-element box, so a select's chevron renders at around 150px tall.

// Or import only the component styles you actually use.
// The theme layer comes with each entry (deduped) — you never @use it by hand.
@use 'ngwr/density';  // the --wr-density-* multipliers; nothing else declares them
@use 'ngwr/icon';     // .wr-icon__svg — sizes every inline chevron and caret
@use 'ngwr/overlay';  // .wr-overlay-sheet — the responsive bottom sheet

@use 'ngwr/button';
@use 'ngwr/select';   // its panel is a <wr-option> list, so this covers both
@use 'ngwr/dialog';   // needed by WrDialog.open(), which no template names

Three entries you will not think to add, and every app wants all three.ngwr/icon — twenty-eight entry points render either a <wr-icon> or a bare inline <svg class="wr-icon__svg"> — select and cascader chevrons, table sort and expand arrows, tree twisties, accordion carets, the alert and toast status marks — and this entry is the only thing that sizes them. ngwr/density — the --wr-density-* multipliers live there and nowhere else; every reader falls back to 1, so without it geometry is correct at md and provideWrDensity() / [wrDensity] silently do nothing at all. ngwr/overlay.wr-overlay-sheet, the mobile bottom-sheet presentation shared by dialog, select, dropdown and popover.

Then the entry point is named after itself, not after the selector. Mostly they agree, and where they do not the surprises are worth reading once: <wr-btn> is @use 'ngwr/button', <wr-tag> is ngwr/badge, <wr-option> is ngwr/select, <wr-accordion> is ngwr/collapse, <wr-form-field> is ngwr/form, <wr-sortable-list> is ngwr/drag-drop, <wr-kbd> is ngwr/keyboard, <wr-count-up> is ngwr/counter. The rule that always holds: it is the last segment of the component's docs URL, the same string as its TypeScript subpath.

And a service mounts a component no template mentions.WrDialog.open() needs @use 'ngwr/dialog', WrToast.show() needs ngwr/toast, and WrDrawerManager.open() needs ngwr/drawer — a dialog without its entry opens as unstyled content with no panel and no backdrop.

Below: everything a component renders beyondngwr/icon, which the paragraph above already settles. The list is DIRECT, so follow it through — <wr-table> pulls ngwr/pagination, which pulls ngwr/select, which pulls ngwr/overlay. A component not listed here needs only its own entry.

Using
Also load
Because it renders
action-sheetngwr/drawer the sheet IS a drawer docked to the bottom edge
avatarngwr/spinner the loading state while an image resolves
badgengwr/spinner a tag in its processing state
buttonngwr/spinner the [loading] state
color-pickerngwr/segmented the HEX / RGB / HSL strip
date-pickerngwr/calendar, ngwr/input the popup panel and the field it opens from
dialogngwr/overlay the responsive bottom-sheet presentation
dropdownngwr/overlay the responsive bottom-sheet presentation
event-calendarngwr/button the view switcher and the month arrows
input-numberngwr/input the field under the steppers
paginationngwr/button, ngwr/select the page cells, and the size changer
popconfirmngwr/button confirm and cancel
popoverngwr/overlay the responsive bottom-sheet presentation
pull-to-refreshngwr/spinner the release indicator
selectngwr/overlay the responsive bottom-sheet presentation
statisticngwr/counter the animated value
tablengwr/checkbox, ngwr/dropdown, ngwr/pagination, ngwr/spinner selection column, filter menu, pager and loading state — all inputs on the one component
tourngwr/button back / next / done in the step popup
transferngwr/button, ngwr/checkbox, ngwr/input the move buttons, the item rows and the search field

The subpath is the API; a deeper path into the package is not.@use 'ngwr/button' resolves through the sass condition in the package's exports map, and that map is what a major has to keep working. A deep path like @use 'ngwr/button/styles/index' happens to compile anyway — Angular's Sass integration resolves files inside a package directly rather than through exports — and nothing is promised about it: the file layout under an entry point is free to move in any release, and Node's own resolution would have refused it. Write the subpath.

If you are not chasing bytes, use the umbrella.

The umbrella has none of this failure mode, and the whole of the saving is CSS — the per-component split changes nothing about your JavaScript bundle, which is decided by what you import in TypeScript. Opt in per component when the stylesheet is genuinely the budget you are over, and check the rendered control rather than the build log, because nothing here fails loudly.

What the opt-in utilities do

Neither is loaded by @use 'ngwr' and neither is a no-op. ngwr/grid adds .grid / .container / .col-* and touches nothing else. ngwr/reset is the one to read before adopting: it is a reset in the modern sense, so it removes browser defaults from elements ngwr does not own, and two of those removals are routinely a surprise — links lose their underline (a { color: inherit; text-decoration: none }) and plain buttons lose their chrome (button { background: none; border: 0; padding: 0 }). It also sets heading sizes and weights from the type scale, which makes it more opinionated than the “box-sizing and body margin” it is usually reached for. Load it AFTER the theme, or after @use 'ngwr': it reads --wr-font-family-base and --wr-text-*, so on its own those resolve to nothing.

// styles.scss — reset AFTER the tokens it reads.
@use 'ngwr';
@use 'ngwr/reset';

// What it changes outside ngwr's own components:
//   *, ::before, ::after   box-sizing: border-box
//   html                   line-height: 1.5, tab-size: 4, text-size-adjust
//   body                   margin: 0, min-height: 100vh, --wr-font-family-base
//   h1..h6                 margin: 0, --wr-text-* sizes, semibold, tight leading
//   p, figure, blockquote, dl, dd   margin: 0
//   a                      color: inherit; text-decoration: none   ← underlines go
//   button                 background: none; border: 0; padding: 0 ← chrome goes
//   button/input/select/textarea    font: inherit; color: inherit
//   img, picture, svg, video        display: block; max-width: 100%
//   ul[role='list'], ol[role='list']  list-style: none; margin/padding: 0
//   code, kbd, samp, pre   --wr-font-family-mono

Use a component

Import the component class into the standalone component's imports array. Nothing here needs a provider or an extra package.

import { Component } from '@angular/core';
import { WrButton } from 'ngwr/button';

@Component({
  selector: 'app-root',
  imports: [WrButton],
  template: `
    <wr-btn color="primary">Save</wr-btn>
  `,
})
export class App {}

Use a component with an icon

Icons are registered, not bundled wholesale: name the ones you use and the rest tree-shake away. This is the first snippet on the page that needs a package beyond ngwr + @angular/cdk. The registry holds WrIconDef values rather than component references, so the bundler drops every lucide export you did not name — but that is a claim about your bundle, not about your build.

// Same button with an icon. Needs the `lucide` peer installed
// (`ng add` does it; `npm install ngwr @angular/cdk` does not).
import { Component } from '@angular/core';
import { Check } from 'lucide';
import { WrButton } from 'ngwr/button';
import { provideWrIcons } from 'ngwr/icon';
import { lucideIcons } from 'ngwr/icon/adapters/lucide';

@Component({
  selector: 'app-root',
  imports: [WrButton],
  providers: [provideWrIcons(lucideIcons({ checkmark: Check }))],
  template: `
    <wr-btn color="primary" icon="checkmark">Save</wr-btn>
  `,
})
export class App {}

Importing from the lucide barrel costs build time, every build.

lucide has no exports map and ships one ES module per icon — over 3 500 of them — behind a single barrel. Naming one icon makes the bundler walk the whole barrel to prove the other 3 500 are unused: they are dropped from the output, having been read on the way there, and a cold Angular build goes from seconds to tens of seconds with no change in bundle bytes. Nothing about ngwr causes it and nothing about ngwr can fix it. If it hurts, register the SVG directly with svgIcon() from ngwr/icon — the route every other icon set on this site takes, tabler and phosphor and heroicons among them — which costs a string per icon and no barrel walk at all.

// Same icon, no lucide barrel to walk: register the SVG as a string.
// svgIcon() takes the markup verbatim, so any icon set that ships plain
// .svg files works — and so does an in-house one.
import { Component } from '@angular/core';
import { WrButton } from 'ngwr/button';
import { provideWrIcons, svgIcon } from 'ngwr/icon';

const CHECK = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 6 9 17l-5-5"/></svg>';

@Component({
  selector: 'app-root',
  imports: [WrButton],
  providers: [provideWrIcons([svgIcon('checkmark', CHECK)])],
  template: `
    <wr-btn color="primary" icon="checkmark">Save</wr-btn>
  `,
})
export class App {}

What an entry point pulls in with it

Two structural facts, since “tree-shakable” is easy to over-read. One: an entry point's static imports come with it, options or no options. A <wr-table> with three columns and nothing turned on still bundles ngwr/checkbox, ngwr/dropdown, ngwr/pagination (and through it ngwr/select and ngwr/button), ngwr/spinner and @angular/cdk/drag-drop — the selection column, the filter menu, the pager and column drag-reorder are inputs on one component, not separate imports, so the code is reachable whether or not the template asks for it. Two: provideWrOverlay() is the heaviest provider in the catalog, because what it pulls is CDK's overlay module, not ngwr code — it is optional in the sense that panels open without it, not in the sense that it is small. Any overlay component pulls that module in anyway, so the marginal cost is near zero once one dialog is on the page and close to the whole of it in an app that has none. Measure yours rather than budgeting from a page; the two commands below are the ones that answer it.

# What one entry point costs in YOUR app, rather than in a doc page.
ng build --configuration production --stats-json
npx source-map-explorer dist/<app>/browser/*.js

# Or diff the two builds directly: add the import, rebuild, compare main.js.
# Angular's own "estimated transfer size" runs 10-20% under gzip -9 —
# if you are checking against a budget, gzip the file yourself.

Override design tokens

Every visual is driven by --wr-* CSS variables. Override them anywhere — :root for app-wide, any subtree for scoped.

// Override theme tokens by redeclaring CSS variables after the lib styles.
@use 'ngwr';

:root {
  --wr-color-primary: #6366f1;       // your brand
  --wr-border-radius-base: 0.5rem;   // tighter or rounder
  --wr-font-family-base: 'Inter', sans-serif;
}

Override the palette at compile time

If you'd rather change the source-of-truth colors and let NGWR regenerate every variant (contrast / dark / light / rgb), configure the SCSS module.

// Or override the whole palette at SCSS compile time.
// Configuring a Sass map REPLACES it, so list every intent you want to exist —
// an omitted key leaves `--wr-color-<intent>` undefined in light mode.
@use 'ngwr/theme' with (
  $base-colors: (
    primary: #6366f1,
    secondary: #ec4899,
    success: #10b981,
    warning: #f59e0b,
    danger: #ef4444,
    info: #3b82f6,
    light: #e5e7eb,
    medium: #6b7280,
    dark: #111827,
  ),
);

What is in the package

Worth a look before you vendor node_modules or go hunting for a file that is not there. node_modules/ngwr holds one FESM bundle per entry point under fesm2022/, a .d.ts per entry point under types/ (types/ngwr-date-picker.d.ts — the fastest exact answer for any API question), the .scss sources every @use 'ngwr/<name>' resolves against, the schematics/ collection ng add and ng update run, and the ngwr-mcp server under mcp/. Roughly half the unpacked size is source maps — one .mjs.map beside every .mjs, each carrying sourcesContent — which is why the unpacked package is roughly twice the size of the code it runs; they are what makes a stack trace inside ngwr readable, and nothing else reads them.

Four markdown files ship, and they are not the docs.README.md, skills/ngwr/SKILL.md and its two references/ files. There is no CHANGELOG.md and no migration guide in the package: the release notes live on /start/migration and in the GitHub releases, so an air-gapped checkout of node_modules does not carry them. The markdown twin of a docs page is a URL, not a file — append .md to any page on this site (/reference/components/select.md) and the server renders it; the MCP server fetches those same URLs rather than reading anything local.

See also