Skip to content
Back to skills

Angular Theming

ASecurity

Multi-theme support in Angular apps — light/dark/brand and per-tenant theming via CSS custom property design tokens, runtime theme switching without reload, Angular Material M3 theming (v17+), persisted theme preference with flash-of-wrong-theme prevention, and WCAG contrast validation across theme variants.

  • 2 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 6, 2026
developmenttypescriptgoreactangularazureapi

Works with

  • cli
  • api

Security analysis

A100/100

Scanned September 6, 2026

npx -y skills add AgenticPawan/FullStack-Pilot --skill angular-theming --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Angular Theming?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Angular Theming
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/agenticpawan-angular-theming/badge)](https://www.skillsdirectory.com/skills/agenticpawan-angular-theming)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: angular-theming
description: Multi-theme support in Angular apps — light/dark/brand and per-tenant theming via CSS custom property design tokens, runtime theme switching without reload, Angular Material M3 theming (v17+), persisted theme preference with flash-of-wrong-theme prevention, and WCAG contrast validation across theme variants.
when_to_use: theming, dark mode, light mode, brand theme, per-tenant branding, design tokens, CSS custom properties, mat.theme, M3 theming, theme switcher, localStorage theme, flash of unstyled theme, FOUC, contrast ratio, WCAG contrast, theme persistence
applies_to: angular>=15
---

<!-- Version index:
  CSS custom properties (design tokens)   all Angular versions (platform CSS feature)
  mat-core / mat.core-theme legacy mixins  Angular Material <17 (pre-M3)
  mat.theme() / system-level tokens        Angular Material 17+ (M3), stable from Material 18
  Signals for reactive theme state         Angular 17+
-->

## Standard IDs

| ID | Severity | What it checks |
|----|----------|----------------|
| ATH-001 | P1 | Hardcoded hex/rgb colors in component SCSS instead of theme tokens |
| ATH-002 | P1 | No centralized theme-token partial — colors/mixins duplicated per component |
| ATH-003 | P0 | Theme switch reloads/re-bootstraps the app instead of toggling a class/attribute |
| ATH-004 | P2 | Legacy pre-M3 `mat-core`/`mat-theme` mixins used on a Material 17+ project |
| ATH-005 | P1 | Theme preference not persisted, or no flash-of-wrong-theme prevention |
| ATH-006 | P0 | Theme token change not validated against WCAG contrast ratios |

---

## Check A — Hardcoded colors vs. design tokens (ATH-001)

### Detection
1. Grep component `.scss` files for raw hex (`#[0-9a-fA-F]{3,8}`) or `rgb(`/`rgba(` literals outside a single `_tokens.scss`/`_theme.scss` partial.
2. Flag any color literal that is not a reference to a CSS custom property (`var(--...)`) or an SCSS variable imported from the shared token file.
3. Confirm a `:root` or theme-class block defines the token set once; components should only *consume* tokens.

### BAD — hardcoded hex scattered in component styles
```scss
// feature-card.component.scss
.feature-card {
  background: #ffffff;
  border: 1px solid #e0e0e0;
  color: #1a1a1a;

  &.is-active {
    background: #eef4ff;
    border-color: #3366ff;
  }
}
```

### GOOD — component consumes centralized design tokens
```scss
// _tokens.scss (imported once into styles.scss)
:root {
  --color-surface: #ffffff;
  --color-border: #e0e0e0;
  --color-text: #1a1a1a;
  --color-surface-active: #eef4ff;
  --color-border-active: #3366ff;
}

:root[data-theme='dark'] {
  --color-surface: #1e1e1e;
  --color-border: #3a3a3a;
  --color-text: #f0f0f0;
  --color-surface-active: #1a2a4a;
  --color-border-active: #6699ff;
}
```

```scss
// feature-card.component.scss
.feature-card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  color: var(--color-text);

  &.is-active {
    background: var(--color-surface-active);
    border-color: var(--color-border-active);
  }
}
```

---

## Check B — Centralized theme-token layer (ATH-002)

### Detection
1. Search for repeated Angular Material mixin invocations (`mat.define-palette`, `@include mat.button-theme`, etc.) inside more than one component-level `.scss` file.
2. Confirm a single `libs/shared-ui/theme` (or `src/styles/theme`) partial owns palette, typography, and spacing tokens, and every other file only imports from it.
3. Flag any per-component redefinition of a palette or typography scale that duplicates the central one.

### BAD — Material theme mixins duplicated per component
```scss
// order-list.component.scss
@use '@angular/material' as mat;

$order-primary: mat.define-palette(mat.$blue-palette);
@include mat.button-theme($order-primary);
```

```scss
// invoice-list.component.scss
@use '@angular/material' as mat;

$invoice-primary: mat.define-palette(mat.$blue-palette); // duplicated palette
@include mat.button-theme($invoice-primary);
```

### GOOD — one theme partial, components only consume tokens
```scss
// src/styles/_theme.scss — single source of truth
@use '@angular/material' as mat;

html {
  color-scheme: light dark;

  @include mat.theme((
    color: (
      theme-type: light,
      primary: mat.$azure-palette,
      tertiary: mat.$blue-palette,
    ),
    typography: Roboto,
    density: 0,
  ));
}
```

```scss
// order-list.component.scss — no palette re-declared, only layout
.order-list {
  padding: var(--spacing-md);
  color: var(--mat-sys-on-surface);
}
```

---

## Check C — Runtime theme switching without reload (ATH-003)

### Detection
1. Search for `window.location.reload()`, `location.href = ...`, or `bootstrapApplication` called again inside a theme-toggle handler.
2. Confirm theme switching instead toggles a `data-theme` attribute or class on `<html>`/`<body>` and lets CSS custom properties cascade.
3. Confirm the toggle is driven by a signal so consuming components re-render only where needed (not full page re-render).

### BAD — full reload to apply a new theme
```typescript
@Component({ selector: 'app-theme-toggle', template: `<button (click)="switchTheme()">Toggle theme</button>` })
export class ThemeToggleComponent {
  switchTheme() {
    localStorage.setItem('theme', 'dark');
    window.location.reload(); // discards app state just to repaint colors
  }
}
```

### GOOD — attribute toggle on <html>, no reload
```typescript
// theme.service.ts
import { Injectable, signal, effect } from '@angular/core';

export type ThemeName = 'light' | 'dark';

@Injectable({ providedIn: 'root' })
export class ThemeService {
  readonly theme = signal<ThemeName>(this.readInitialTheme());

  constructor() {
    effect(() => {
      document.documentElement.setAttribute('data-theme', this.theme());
      localStorage.setItem('theme', this.theme());
    });
  }

  toggle(): void {
    this.theme.update(t => (t === 'light' ? 'dark' : 'light'));
  }

  private readInitialTheme(): ThemeName {
    return (localStorage.getItem('theme') as ThemeName) ?? 'light';
  }
}
```

```typescript
@Component({
  selector: 'app-theme-toggle',
  template: `<button (click)="theme.toggle()">Toggle theme</button>`,
})
export class ThemeToggleComponent {
  protected readonly theme = inject(ThemeService);
}
```

---

## Check D — Legacy pre-M3 Material mixins on M3-capable projects (ATH-004)

**Applies only when `@angular/material` >= 17 is detected in `package.json`.**

### Detection
1. Check `package.json` for `@angular/material` major version.
2. If >= 17, grep global styles for `@include mat.core(` (legacy) or `mat.define-light-theme` / `mat.define-dark-theme` combined with `mat.all-component-themes` — these are the pre-M3 API.
3. Flag as advisory-to-required migration to `mat.theme()` / system tokens, since M3 is the default for new Material 17+ projects (`ng generate @angular/material:m3-theme`).

### BAD — legacy mat-core/mat-theme API on Material 18 project
```scss
@use '@angular/material' as mat;

@include mat.core(); // pre-M3 legacy setup

$my-primary: mat.define-palette(mat.$indigo-palette);
$my-theme: mat.define-light-theme((
  color: (primary: $my-primary, accent: mat.define-palette(mat.$pink-palette)),
));

@include mat.all-component-themes($my-theme);
```

### GOOD — M3 system-level token API
```scss
@use '@angular/material' as mat;

html {
  @include mat.theme((
    color: (
      theme-type: light,
      primary: mat.$violet-palette,
      tertiary: mat.$magenta-palette,
    ),
    typography: Roboto,
    density: 0,
  ));
}

html[data-theme='dark'] {
  @include mat.theme((
    color: (theme-type: dark, primary: mat.$violet-palette),
  ));
}
```

---

## Check E — Persisted preference and flash-of-wrong-theme prevention (ATH-005)

### Detection
1. Confirm the theme is read from `localStorage` (or a user profile call) before the first paint, not only after Angular bootstraps.
2. Grep `index.html` for an inline bootstrap `<script>` that sets `data-theme` synchronously; its absence on an app with dark mode is a flag.
3. Confirm `ThemeService` restores the saved value on init rather than always defaulting to `'light'`.

### BAD — theme decided only after Angular bootstraps (visible flash)
```html
<!-- index.html -->
<html>
  <head></head>
  <body>
    <app-root></app-root>
    <!-- theme class only applied once ThemeService runs inside Angular -->
  </body>
</html>
```

### GOOD — inline script sets theme before first paint, service restores state
```html
<!-- index.html -->
<html>
  <head>
    <script>
      (function () {
        var saved = localStorage.getItem('theme');
        var theme = saved || (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
        document.documentElement.setAttribute('data-theme', theme);
      })();
    </script>
  </head>
  <body>
    <app-root></app-root>
  </body>
</html>
```

```typescript
// theme.service.ts — restores the same value the inline script already applied
private readInitialTheme(): ThemeName {
  return (document.documentElement.getAttribute('data-theme') as ThemeName)
    ?? (localStorage.getItem('theme') as ThemeName)
    ?? 'light';
}
```

---

## Check F — WCAG contrast validation across theme variants (ATH-006)

### Detection
1. For every theme variant (`light`, `dark`, per-tenant brand), compute contrast ratio of each text token against its paired surface token.
2. Flag any body-text pair below 4.5:1 (WCAG AA, normal text) or large-text/UI pair below 3:1.
3. Confirm contrast checks run in CI (e.g., via `axe-core` or a token-contrast lint script), not only eyeballed once at design time.

### BAD — dark theme background changed without re-checking text contrast
```scss
:root[data-theme='dark'] {
  --color-surface: #12141a;
  --color-text: #4a4f5a; /* looks fine on old bg, now ~2.1:1 on new bg — fails AA */
}
```

### GOOD — token change re-validated and adjusted to meet AA
```scss
:root[data-theme='dark'] {
  --color-surface: #12141a;
  --color-text: #e8eaf0; /* ~13.8:1 against --color-surface — passes AA and AAA */
}
```

```typescript
// tools/check-contrast.ts — run in CI against every theme's token pairs
import { getContrastRatio } from './contrast-utils';

const pairs: Array<[fg: string, bg: string, name: string]> = [
  ['#e8eaf0', '#12141a', 'dark: text on surface'],
];

for (const [fg, bg, name] of pairs) {
  const ratio = getContrastRatio(fg, bg);
  if (ratio < 4.5) {
    throw new Error(`Contrast failure (${name}): ${ratio.toFixed(2)}:1, need >= 4.5:1`);
  }
}
```

---

## Theming checklist

- [ ] No hardcoded hex/rgb values in component SCSS — all colors reference `var(--token-name)`
- [ ] One central theme-token partial owns palettes, typography, and spacing; components only consume
- [ ] Theme switching toggles `data-theme`/class on `<html>` — no `location.reload()` or re-bootstrap
- [ ] Material 17+ projects use `mat.theme()` system tokens, not legacy `mat.core()`/`define-light-theme`
- [ ] Theme preference persisted to `localStorage` (or user profile) and restored on load
- [ ] Inline bootstrap script sets `data-theme` before first paint to prevent flash-of-wrong-theme
- [ ] Every theme variant's text/surface token pairs pass WCAG AA contrast (4.5:1 normal, 3:1 large/UI)
- [ ] Contrast checks are automated in CI, not manual-only

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…