UI scale
@flyze/lib-core-angular 1.0.0-alpha.34· latestFlyzeUiScaleService scales the whole UI by one number: the root font size. Every dimension in the
Flyze component libraries is authored in rem, so writing
documentElement.style.fontSize = '125%' re-lays-out text, control heights, icons and gaps together
and proportionally.
scale.setScale(125); // everything authored in rem grows by a quarterscale.scale; // 125scale.scale$; // Observable<number>The service is providedIn: 'root' and needs no setup. It renders nothing, stores nothing, and has
no UI of its own — building the setting is the app’s job:
guides/scaling-an-app. Why the root font size and why not the theme:
ADR 0010.
What actually scales
Section titled “What actually scales”| Metric | authored | at 100% | at 125% | at 150% |
|---|---|---|---|---|
button medium height | rem(24) | 24px | 30px | 36px |
| its font size | rem(12) | 12px | 15px | 18px |
| its icon (derived) | — | 16px | 20px | 24px |
| its focus ring | 1.5px | 1.5px | 1.5px | 1.5px |
The whole proportional tuple moves, not just the text: 15px text inside a still-24px box would overflow, and the click target would stay physically small on the very display that motivates scaling. Hairlines, focus rings, Angular Material’s own metrics and third-party chrome deliberately stay put — a 1px border at 1.25× renders as a smeared grey line.
Components need no cooperation to render at a new scale: rem resolves against the root at style
recalc, so the browser reflows on its own and live preview is free. scale$ exists only for the few
places that cached a pixel measurement in TypeScript.
The four layers that multiply
Section titled “The four layers that multiply”The physical size a reader sees is OS display scaling × browser zoom × browser font-size preference × this scale. Every layer multiplies; none is overridden. That is why the service writes a percentage:
| Root rule | Default reader (16px) | Reader who set 20px in the browser |
|---|---|---|
font-size: 125% | 1rem = 20px | 1rem = 25px — both preferences apply |
font-size: 20px | 1rem = 20px | 1rem = 20px — theirs is discarded |
Consequence for TypeScript: scale is not a conversion factor. At a neutral 100%, a reader with
a 20px browser setting still has 1rem = 20px. Anything that needs a real pixel length reads the
resolved value instead:
scale.rootFontSize; // resolved length of 1rem in px, right nowscale.resolvedScaleFactor; // that over 16 — the factor an authored px value really getsBoth are snapshots, not streams, on purpose: the browser preference can change with no event this
service could observe, so a stream would go stale silently. Best of all, measure the element you care
about (clientHeight) — it is read after the browser resolved rem to px and is therefore correct at
any scale, with no scale-awareness in the component.
The API in detail
Section titled “The API in detail”setScale(percent)
Section titled “setScale(percent)”Rounds to whole percent, clamps to 50–200, writes the DOM, and emits — in that order, and only when the value actually changed.
Three behaviors worth knowing:
- At neutral it clears the inline style rather than writing
100%(#writeRootFontSize), so an app at 100% is byte-identical to one that never touched scaling and the reader’s own browser setting fully governs the baseline.resetScale()is the same thing, named. - A non-finite value is rejected with a warning and changes nothing. Without that guard
NaNsurvivesMath.round/min/max, the DOM write becomes'NaN%'— silently dropped by the CSSOM — and the service would reportNaNas the current scale. - Out-of-range values are clamped, not rejected.
setScale(400)leaves you at 200 and reports 200.
Adopting a scale applied before bootstrap
Section titled “Adopting a scale applied before bootstrap”An app is supposed to apply its persisted scale before Angular renders, or every load flashes at
neutral and jumps. So on construction the service reads documentElement.style.fontSize:
- a percentage → adopted (and re-clamped, so a stale
400%is repaired to200%); - anything else, e.g.
20px→ left in place, with a warning. Clearing a value the app set for its own reasons, on mere injection, would be worse than the disagreement it warns about; - empty → neutral, nothing written.
This is the one place the service reads the DOM instead of owning it, and it exists so scale can
never disagree with what is actually applied.
pxToRem / remToPx
Section titled “pxToRem / remToPx”scale.pxToRem(12); // '0.75rem' — a px value authored at neutral, as a CSS lengthscale.remToPx(1); // 20 — resolved at the current scalepxToRem is the TypeScript counterpart of the component libraries’ SCSS fn.rem() and shares its
16px baseline. It is scale-independent by design — the browser applies the scale when it resolves
the rem, so doing it here too would apply it twice. Use it wherever a numeric input means “px at
100%” and the value ends up in a style:
element.style.height = scale.pxToRem(config.minHeight); // 58 -> '3.625rem'remToPx goes the other way, against the live rootFontSize, for the cases that must hand a plain
number to something that cannot take a CSS length — a CDK virtual-scroll itemSize, say. The result
is a snapshot: recompute it when scale$ emits.
Both warn and return a zero value ('0rem' / 0) for a non-finite input rather than propagating
NaN into layout math.
Boundaries
Section titled “Boundaries”- The service claims the inline root font size exclusively. An app must not write
documentElement.style.fontSizeitself; ahtml { font-size }rule in a stylesheet is overridden by it whenever the scale is not neutral. - No persistence, no boot hook, no UI. All three belong to the app, exactly as the color scheme’s do — see the guide.
- The 16px baseline is not a design choice. It is what a browser defines
1remto be at its default setting, and it is the number everyremdimension in the component libraries was converted against ($base-font-sizein theirstyles/_functions.scss). The two must stay equal; redefiningFLYZE_UI_SCALE_BASE_FONT_SIZEwould resize every component. - This library has no
rem-authored components of its own to scale. It ships the knob for the component libraries, which must not depend on it — their half of the contract is beingrem-clean, nothing more.