Angular

NibCSS in Angular through the official builders: no plugin file, no second process — plus the directive, service, pipe and dark-mode toggle.

3 min readv1

Angular hides its bundler, so @nibcss/angular ships builders instead of a plugin: swap the two targets in angular.json and ng serve / ng build keep working exactly as before.

1. Create a project#

Terminal
npm i -g @angular/cli      # or use npx
ng new my-app --style=css
cd my-app
ng serve                   # http://localhost:4200

2. Install#

Terminal
npm i @nibcss/angular @nibcss/runtime

3. Point the two targets at NibCSS#

JSON
// angular.json
{
  "architect": {
    "build": {
      "builder": "@nibcss/angular:application",
      "options": { /* …exactly what you had before… */ }
    },
    "serve": {
      "builder": "@nibcss/angular:dev-server",
      "configurations": { /* …unchanged… */ }
    }
  }
}

That is the whole setup — no second process, no @import to maintain, no plugin file to write. Every option @angular/build:application and @angular/build:dev-server accept is accepted here unchanged and passed straight through.

What the builders do:

  • read nibcss.config.ts, so every option in it applies exactly as it does in a Vite project
  • scan your templates and components, write the stylesheet, and add it to styles ahead of your own, so your CSS always wins
  • publish the resolved config as @nibcss/config and compile it into the bundle, so a dynamic [nib] binding resolves to the very class names the stylesheet holds
  • rebuild when a template or the config changes, without restarting the server
  • with debug: true, print each problem with its file and line, and mount the overlay on ng serve only — a production bundle never carries it

A nib block on the build target covers only where files are; everything about styling stays in nibcss.config.ts:

JSON
"options": {
  "nib": {
    "out": "src/nib.css",
    "config": "nibcss.config.ts",
    "include": ["src/**/*.{html,ts}"],
    "exclude": ["**/*.spec.ts"],
    "devtools": true,
    "scaffold": true
  }
}

Run ng serve once and nibcss.config.ts is created at the project root, with src/nib.css and src/nib.config.mjs written beside your sources — the half of the project Angular's watcher actually looks at.

4. Provide the runtime#

TypeScript
// src/app/app.config.ts
import { ApplicationConfig } from "@angular/core";
import { provideNib } from "@nibcss/angular";

export const appConfig: ApplicationConfig = {
  providers: [provideNib()],
};

provideNib() with no argument is correct: the builder compiles the resolved config into the bundle, so the runtime already knows your prefix, tokens and recipes. Pass a partial to override part of it — provideNib({ tokens: { color: { accent: "#f0f" } } }) adds to the palette rather than replacing it.

5. A component#

TypeScript
// src/app/app.ts
import { Component, signal } from "@angular/core";
import { NIB_DIRECTIVES, NibService } from "@nibcss/angular";

@Component({
  selector: "app-root",
  imports: [...NIB_DIRECTIVES],
  template: `
    <main nib="mxw=720px; mi=auto; p=$space.xl $space.md; d=flex; flxd=column; g=$space.lg">
      <header nib="d=flex; ali=center; jsc=space-between; g=$space.md">
        <h1 nib="m=0; fts=28px; ftw=800; lts=-.02em">Dashboard</h1>

        <!-- hides itself when dark mode cannot be toggled -->
        <button nib="btn" nibDarkToggle>{{ nib.isDark() ? "Light" : "Dark" }}</button>
      </header>

      <section nib="d=flex; flxd=column; g=$space.md; md:flxd=row">
        <article nib="card-hover; flg=1"><h2 nib="m=0; fts=18px">Revenue</h2></article>
        <article nib="card-hover; flg=1"><h2 nib="m=0; fts=18px">Users</h2></article>
      </section>

      <!-- a value only the runtime can resolve -->
      <input type="range" min="4" max="48" [value]="pad()"
             (input)="pad.set(+$any($event.target).value)"
             nib="w=100%; acc=$color.brand" />
      <div [nib]="'p=' + pad() + 'px; bd=1px solid $color.border; bdrd=$radius.md'">
        padding: {{ pad() }}px
      </div>

      <!-- the record form, like [class] -->
      <div [nib]="{ 'cl=#b91c1c': invalid(), 'ftw=700': true }">state</div>

      <!-- the pipe, when you need a class string -->
      <span [class]="'btn' | nibClass">a class string</span>
    </main>
  `,
})
export class App {
  protected readonly pad = signal(16);
  protected readonly invalid = signal(false);
  constructor(protected readonly nib: NibService) {}
}

6. The API#

Export

What it is

provideNib(options?)

start the runtime as the environment injector is built

[nib]="…"

the directive — a string, a record ({ 'cl=red': cond }) or an array; applies through Renderer2, so it does not fight [class]

NibService

isDark, darkModeStrategy, canToggleDarkMode (signals), toggleDarkMode(), setDarkMode(), useSystemDarkMode(), classes(), token(), configure(), css(), scan(), config, runtime

| nibClass

a class string in a template

nibDarkToggle

toggles on click, and hides itself when darkMode is not "class"

NIB_DIRECTIVES

all three, for a standalone component's imports

NIB_RUNTIME, NIB_OPTIONS

the injection tokens

resetSharedNibRuntime()

drop the shared instance (tests)

mergeConfig, buildInjectedConfig

the config helpers the provider uses

Common pitfalls#

  • Leaving the default @angular/build builders in place — nothing scans your templates, so the stylesheet is never written.
  • Forgetting provideNib(): the CSS exists but no classes are applied.
  • Forgetting imports: [...NIB_DIRECTIVES] in a standalone component — [nib] is then just an unknown attribute.
  • Expecting [nib] and [class] to conflict — they do not; the directive applies through Renderer2.

Next steps#