Monsoon v0.2.0
Made by Alexandre Kohler Star

Installation

Install Monsoon in Angular

Angular has three good places for a global stylesheet: the styles array in angular.json, an @import in src/styles.css, or a <link> in src/index.html. Templates then use plain class attributes plus [class.x] and [style.--seed] bindings.

2 min readAngular

Install

src/index.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>MyApp</title>
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/monsooncss@0.2.0/dist/monsoon.min.css">
  <script>(function(){var t;try{t=localStorage.getItem('monsoon-theme')}catch(e){}var d=t==='dark'||(t!=='light'&&matchMedia('(prefers-color-scheme: dark)').matches);document.documentElement.classList.add(d?'dark':'light')})()</script>
</head>
<body class="bg-background text-foreground antialiased">
  <app-root></app-root>
</body>
</html>

Angular budgets measure the uncompressed size of the initial bundle, and the full stylesheet is over the 500 kB warning that new projects set for initial. Raise that budget in the production configuration, or ship the purged file (see below), which is the better fix.

Use the classes

Static classes go in class. For conditional ones, prefer a [class] binding with full strings; Angular merges it with the static attribute:

src/app/plan-card.component.ts
import { Component, input } from '@angular/core';

@Component({
  selector: 'app-plan-card',
  template: `
    <article
      class="rounded-2xl border bg-surface p-6 space-y-4"
      [class]="featured() ? 'ring-2 ring-ring' : 'shadow-sm'"
    >
      <h2 class="text-lg font-semibold">{{ name() }}</h2>
      <p class="text-3xl font-bold">{{ price() }}</p>
      <button
        type="button"
        class="w-full rounded-lg px-4 py-2 font-medium"
        [class.bg-primary]="featured()"
        [class.text-primary-foreground]="featured()"
        [class.bg-secondary]="!featured()"
        [class.text-secondary-foreground]="!featured()"
      >
        Choose {{ name() }}
      </button>
    </article>
  `,
})
export class PlanCardComponent {
  name = input.required<string>();
  price = input.required<string>();
  featured = input(false);
}
src/app/app.component.html
<main class="mx-auto max-w-5xl px-6 py-12">
  <div class="grid grid-cols-1 md:grid-cols-3 gap-6">
    @for (plan of plans; track plan.name) {
      <app-plan-card [name]="plan.name" [price]="plan.price" [featured]="plan.featured" />
    }
  </div>
</main>

Both [class.bg-primary] and the [class] string work at runtime. The difference matters for the purge step below.

Dark mode

A root service owns the scheme. It reads the class the head script set, and toggles it through the injected DOCUMENT:

src/app/theme.service.ts
import { Injectable, inject, signal } from '@angular/core';
import { DOCUMENT } from '@angular/common';

@Injectable({ providedIn: 'root' })
export class ThemeService {
  private readonly root = inject(DOCUMENT).documentElement;
  readonly theme = signal<'light' | 'dark'>(this.root.classList.contains('dark') ? 'dark' : 'light');

  toggle(): void {
    const next = this.theme() === 'dark' ? 'light' : 'dark';
    this.root.classList.remove('dark', 'light');
    this.root.classList.add(next);
    try { localStorage.setItem('monsoon-theme', next); } catch (e) {}
    this.theme.set(next);
  }
}
src/app/theme-toggle.component.ts
import { Component, inject } from '@angular/core';
import { ThemeService } from './theme.service';

@Component({
  selector: 'app-theme-toggle',
  template: `
    <button
      type="button"
      class="rounded-lg border px-3 py-2 text-sm hover:bg-muted"
      [attr.aria-pressed]="theme.theme() === 'dark'"
      (click)="theme.toggle()"
    >
      {{ theme.theme() === 'dark' ? 'Light mode' : 'Dark mode' }}
    </button>
  `,
})
export class ThemeToggleComponent {
  protected readonly theme = inject(ThemeService);
}

To force one region dark, bind the class on it: <aside class="bg-background text-foreground p-6" [class.dark]="alwaysDark">.

Theme with --seed

Angular binds custom properties with the [style.--name] syntax:

src/app/brand-preview.component.ts
import { Component, signal } from '@angular/core';

@Component({
  selector: 'app-brand-preview',
  template: `
    <section class="rounded-2xl bg-surface p-6 space-y-4" [style.--seed]="seed()">
      <input type="color" [value]="seed()" (input)="seed.set($any($event.target).value)" />
      <button type="button" class="rounded-lg bg-primary text-primary-foreground px-4 py-2">Primary</button>
    </section>
  `,
})
export class BrandPreviewComponent {
  seed = signal('#0d9488');
}

Every semantic token (primary, secondary, accent, surfaces, muted text, borders and the focus ring) is derived from --seed in the browser. Theming lists the tokens.

Using the purge CLI

package.json
{
  "scripts": {
    "start": "ng serve",
    "css:purge": "monsooncss purge \"src/**/*.{html,ts}\" -o src/monsoon.purged.css",
    "build": "npm run css:purge && ng build"
  }
}

Then swap the stylesheet in the production configuration only, so ng serve keeps the full file:

angular.json
{
  "projects": {
    "my-app": {
      "architect": {
        "build": {
          "configurations": {
            "production": {
              "styles": [
                "src/monsoon.purged.css",
                "src/styles.css"
              ]
            }
          }
        }
      }
    }
  }
}

The purge CLI does not read [class.bg-primary] as the class bg-primary, because the binding name and the class are one token. For classes you only use in [class.x] bindings, either switch to a [class] string as in the card above, or list them in --safelist bg-primary,text-primary-foreground.

The purge CLI reads your files as plain text and keeps every class name it can see. It cannot see a name that is assembled at runtime, so write each class in full: isActive ? 'bg-primary' : 'bg-muted' survives a purge, 'bg-' + color does not. Names that only exist at runtime (from a database, a CMS or an API) go in --safelist bg-success,bg-danger.

The purged file always keeps the reset, the theme tokens, the .dark and .light scopes and the keyframes, so dark mode and --seed keep working. See the purge CLI reference for every option.

Next steps

Frequently asked questions

Which install option should I pick in Angular?

The styles array is the most explicit and keeps the file in the build output. The @import keeps everything in CSS. The CDN link keeps the stylesheet out of the bundle and out of the budget.

Does Monsoon work with Angular SSR?

Yes. The stylesheet has no runtime. Keep the head script in src/index.html so the scheme is set before the server rendered HTML paints.

No recent searches

Try , , or .

Recent

Loading the search index

No results for ""

Check the spelling, or search a class name such as px-4.