# WrTheme

> Light / dark theme manager. Writes `[data-theme]` on `<html>`, persists the chosen mode to localStorage. `auto` mode follows `WrPlatform.prefersDark`.

Source: https://ngwr.dev/reference/services/theme  
Kind: Service

## Install

```angular-ts
import { provideWrTheme, WrTheme } from 'ngwr/theme';

bootstrapApplication(AppComponent, {
  providers: [provideWrTheme({ defaultMode: 'auto' })],
});
```

## Usage

```angular-ts
private readonly theme = inject(WrTheme);

this.theme.set('dark');
this.theme.toggle();

protected readonly resolved = this.theme.resolved; // Signal<'light' | 'dark'>
protected readonly mode = this.theme.mode;         // Signal<'light' | 'dark' | 'auto'>
```

## SSR: no flash of the wrong theme

A server has no `localStorage` and no `prefers-color-scheme`, so every server-rendered or prerendered page ships the same attribute — `light` under the default config. A visitor whose OS is dark, or who chose dark here, sees a white page until the client bundle boots and `WrTheme` corrects it: milliseconds on localhost, seconds on a slow connection. No provider can close that window, because by the time Angular runs the browser has already painted.

The fix is a blocking script in `<head>`, above every stylesheet, that resolves the theme before first paint. `wrThemePrePaintScript()` emits one — reading the persisted mode exactly the way `WrTheme` and `WrStorage` agree to write it — the JSON envelope, the key prefix and the TTL — so nothing here has to be re-derived from the bundle or kept in step by hand. This page's own output, ready to paste:

```html
<script>(function(){try{var m=null,r=null;try{r=window.localStorage.getItem("wr-theme");}catch(e){}if(r!==null){try{var p=JSON.parse(r);if(p!==null&&typeof p==="object"&&"v" in p){if(p.e===undefined||p.e>=Date.now())m=p.v;}else{m=p;}}catch(e){m=r;}}if(m!=="light"&&m!=="dark"&&m!=="auto")m="auto";var dark=m==="dark"||(m==="auto"&&typeof window.matchMedia==="function"&&window.matchMedia("(prefers-color-scheme: dark)").matches);document.documentElement.setAttribute("data-theme",dark?"dark":"light");}catch(e){}})();</script>
```

It is plain ES5 with no dependency on the app bundle, and every step is wrapped: a blocked `localStorage` (private mode, a sandboxed iframe) leaves the rendered default in place rather than throwing before anything has rendered. The docs site you are reading runs this same script, and a spec in the library pins the two to the same answers.

```typescript
import { wrThemePrePaintScript } from 'ngwr/theme';

// Same script, for an app that renamed the attribute, the key or the prefix.
// Pass the same values you gave provideWrTheme() / provideWrStorage():
wrThemePrePaintScript({
  attribute: 'app-theme',    // provideWrTheme({ attribute })
  storageKey: 'theme-mode',  // provideWrTheme({ storageKey })
  defaultMode: 'dark',       // provideWrTheme({ defaultMode })
  storagePrefix: 'myapp:',   // provideWrStorage({ prefix })
});

// It returns the JS source with NO <script> wrapper, so an SSR template can
// inline it — and a CSP that forbids 'unsafe-inline' can hash it instead of
// reaching for a nonce:
const source = wrThemePrePaintScript();
const hash = createHash('sha256').update(source).digest('base64');
// Content-Security-Policy: script-src 'sha256-${hash}'
```

## One service per application, one attribute per document

`WrTheme` is root-provided: one instance for the whole application, created by `provideWrTheme()`'s initializer at bootstrap. It reads `WR_THEME_CONFIG` once, from the root injector — so a `provideWrTheme()` in a lazy route's `providers` sets a token the service will never look at, and the route's `defaultMode` or `storageKey` is silently the bootstrap one. Configure it once, at bootstrap. `WrDensity`, `WrI18n`, `WrStorage` and `WrToast` follow the same rule for the same reason; `provideWrConfig()` and `provideWrIcons()` deliberately do not, because the things that read those resolve them from their own injector.

The document, though, has exactly one `<html>` — and that is what matters if you run **two Angular applications on one page** (a micro-frontend shell plus a widget, or two mounted bundles). Each gets its own `WrTheme`; both write the same attribute and both persist to the same `localStorage` key. The attribute is last-writer-wins, and each service's `resolved()` keeps reporting _its own_ answer — so one of them can confidently report `light` while the page is painted dark, and after a reload the theme is decided by whichever application initialised last. Overlay containers do not collide this way; the theme does, because the thing being shared is the document.

```typescript
// Shell — the one application that owns <html data-theme> and the storage key.
bootstrapApplication(ShellComponent, {
  providers: [provideWrTheme({ defaultMode: 'auto' })],
});

// Widget mounted on the same page — no provideWrTheme(), no inject(WrTheme).
// Its ngwr components are themed already: the attribute and the tokens are on
// the document, not on the injector.
bootstrapApplication(WidgetComponent);

// If the widget needs the value in TypeScript, observe the document instead of
// constructing a second owner of it.
const html = document.documentElement;
const theme = signal(html.getAttribute('data-theme') ?? 'light');
new MutationObserver(() => theme.set(html.getAttribute('data-theme') ?? 'light'))
  .observe(html, { attributeFilter: ['data-theme'] });

// Unavoidable second instance? At least stop the persisted choices colliding.
bootstrapApplication(WidgetComponent, {
  providers: [provideWrTheme({ storageKey: null })],
});
```

Note that _writing_ is not limited to `set()`: the service mirrors its resolved theme onto `<html>` from an effect the moment it is constructed, so a second application becomes a writer simply by injecting `WrTheme` to read the current value. The rule that follows is a short one — **exactly one application on the page calls `provideWrTheme()` or injects the service**. The others need nothing: the attribute and the whole `--wr-*` layer live on the document, so their components are already themed, and a widget that must react in TypeScript can watch the attribute rather than own it. Where a second instance is unavoidable, give it a distinct `storageKey` — or `null`, which turns persistence off — so at least the two stop overwriting each other's choice.

## Why ngwr provides this

Theme switching sounds like one line of CSS until you handle persistence, the `prefers-color-scheme` fallback, SSR (no `localStorage`), and flashing the wrong theme on first paint. `WrTheme` owns that lifecycle and exposes the result as signals.

## API

| Name | Description | Type | Default |
| --- | --- | --- | --- |
| `mode` | User-selected mode — `'light' \| 'dark' \| 'auto'`. | `Signal<WrThemeMode>` | `—` |
| `resolved` | Resolved theme actually applied to \<html>. | `Signal<'light' \| 'dark'>` | `—` |
| `set(mode)` | Switch to a specific mode. | `(m: WrThemeMode) => void` | `—` |
| `toggle()` | Flip light ↔ dark (skips auto). | `() => void` | `—` |
| `provideWrTheme(config?)` | Configure defaultMode, storageKey, attribute name. | `(config?) => EnvironmentProviders` | `—` |
| `wrThemePrePaintScript(options?)` | Source of the blocking script that resolves the theme before first paint. Takes the same `attribute` / `storageKey` / `defaultMode` you passed `provideWrTheme()`, plus `storagePrefix` and `json` from `provideWrStorage()`. | `(options?: WrThemePrePaintOptions) => string` | `—` |
