# Density

> One global knob that scales how tightly every control packs — `sm`, `md` (default), `lg`, `touch`. Controls multiply their paddings by a small set of density scalars, so the whole UI tightens or relaxes together without re-rendering.

Source: https://ngwr.dev/guides/tokens/density  
Kind: Tokens

## How it works

Density is four CSS custom properties — multipliers that default to `1`. Setting a density writes `[data-wr-density]` (on `<html>` for the global scale, or on a subtree host for an override), and the stylesheet swaps the multipliers for that scope. Every control reads them through `calc()`, so changing density is a pure CSS-variable flip — no JS re-layout, no component churn. `md` resolves every multiplier to `1`, so touching density is pure opt-in: controls without density-aware paddings just keep their static values.

| Name | Description | Type | Default |
| --- | --- | --- | --- |
| `--wr-density-y` | Scales vertical padding (`padding-y`). The lever that changes control height. | `number` | `1` |
| `--wr-density-x` | Scales horizontal padding (`padding-x`). | `number` | `1` |
| `--wr-density-text` | Declared for consumers to read; no shipped component multiplies by it yet. | `number` | `1` |
| `--wr-density-gap` | Declared for consumers to read; no shipped component multiplies by it yet. | `number` | `1` |

## The scale

`WrDensityValue` is `'sm' | 'md' | 'lg' | 'touch'`, default `md`. Each value maps to a fixed set of multipliers, scoped by the `[data-wr-density='…']` selectors in the density stylesheet.

| Name | Description | Type | Default |
| --- | --- | --- | --- |
| `sm` | Compact — dense tables, toolbars, data-heavy screens. | `y 0.55 · x 0.85` | `text 0.95 · gap 0.8` |
| `md` | Default. Every multiplier resolves to `1` — the baseline control sizing. | `y 1 · x 1` | `text 1 · gap 1` |
| `lg` | Roomy — marketing pages, low-density forms. | `y 1.35 · x 1.15` | `text 1.05 · gap 1.15` |
| `touch` | Finger-friendly (~44px targets). Leans on `y` + `gap`; text stays at reading size since touch is about hit area, not legibility. | `y 1.7 · x 1.25` | `text 1 · gap 1.5` |

## How density meets control sizing

Controls never hardcode their height. Each declares its static `padding-y` / `padding-x` from the shared `--wr-control-*` contract (so a button, input and select line up pixel-for-pixel at every size), then multiplies that padding by the matching density scalar in `calc()`. Because height is `line-height + 2×padding-y + 2×border`, scaling `padding-y` is what actually changes the control's height — which is why `touch` leans almost entirely on `--wr-density-y`.

```scss
// How a control reads the multipliers. The control declares its static
// padding via the shared control-* contract, then multiplies by the
// density scalar in calc() — so it tracks density without re-rendering.
.wr-input {
  padding: calc(var(--wr-input-padding-y) * var(--wr-density-y, 1))
    calc(var(--wr-input-padding-x) * var(--wr-density-x, 1));
}
```

## Global density — provideWrDensity()

Configure the global scale at bootstrap. All fields are optional and merged with the defaults (`defaultDensity: 'md'`, `storageKey: 'wr-density'`, `attribute: 'data-wr-density'`). The chosen value is persisted via `WrStorage` under `storageKey` — pass `storageKey: null` to disable persistence. **The provider has a stylesheet half, and it is not in the theme layer.** The four multipliers are declared in `ngwr/density` and nowhere else, so `@use 'ngwr'` brings them and a per-component setup does not: without `@use 'ngwr/density'` every reader falls through to its `var(--wr-density-y, 1)` fallback, which means the controls look right at `md` and the provider, the service and the directive all appear to do nothing at all. Nothing warns — this is the failure to check first if a density switch has no visible effect.

```angular-ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideWrDensity } from 'ngwr/density';

bootstrapApplication(AppComponent, {
  providers: [
    // All fields optional — merged with defaults.
    // defaultDensity: 'md', storageKey: 'wr-density', attribute: 'data-wr-density'
    provideWrDensity({ defaultDensity: 'sm' }),
  ],
});
```

## Changing it at runtime — WrDensity

Inject the `WrDensity` service to switch density live. `set()` writes `[data-wr-density]` on `<html>` and persists the value, `cycle()` steps through `sm → md → lg → touch`, and `current()` is a `Signal<WrDensityValue>` you can bind to. Unknown values passed to `set()` are ignored.

```angular-ts
import { Component, inject } from '@angular/core';
import { WrDensity } from 'ngwr/density';

@Component({ /* … */ })
export class DensityToggle {
  private readonly density = inject(WrDensity);

  set() {
    this.density.set('lg');   // switch globally — writes [data-wr-density] on <html>
    this.density.cycle();     // sm → md → lg → touch → sm …
    this.density.current();   // Signal<WrDensityValue> — read the active value
  }
}
```

## Per-subtree override — [wrDensity]

Drop the `wrDensity` directive on any element to scope a density to that subtree only — it writes the same `data-wr-density` attribute on its host instead of `<html>`, so the multiplier rules cascade to descendants while the rest of the app keeps its global value.

```angular-html
<!-- The whole sidebar runs at sm; the rest of the app keeps its global density. -->
<aside wrDensity="sm">
  <wr-list ...></wr-list>
</aside>
```

## See also

- [WrDensity](https://ngwr.dev/reference/services/density) — The runtime switcher behind these multipliers — `set()`, `cycle()` and the active signal.
- [Control sizing](https://ngwr.dev/guides/tokens/sizing) — The `--wr-control-*` paddings density scales — a control that does not read them will not track it.
- [Theming](https://ngwr.dev/guides/theming) — Wiring `provideWrDensity()` alongside the theme.
