--- title: RFC — The `scaling` axis (global zoom), separate from density type: rfc audience: human + agent status: implemented source: migrated from src/uix/eidos/SCALING_RFC.md (2026-07-02, docs-book F7.4) --- # RFC — The `scaling` axis (global zoom), separate from density > **Status: ✅ IMPLEMENTED.** (designed 2026-06-01 · implemented 2026-06-02) > Born from P1-2: density-driven typography (`content-scale`) was wrongly framed. > Density and zoom are different things; this RFC introduces zoom as its own axis. > > **Code**: `lib/config-types.ts` (`SCALING_KEYS` / `ScalingKey` / `DEFAULT_SCALING` / > **`ScalingParticipationMap`** — the axis definition, added 2026-07-06), > `lib/primitives/static.ts` (`STATIC_SCALING` + **`STATIC_SCALING_PARTICIPATION`**, > the canonical per-family defaults), `lib/render-css.ts` > (`appendScalingDeclarations` / `appendScaledMetricDeclarations` / `renderScalingBlocks` > — the emitters consume the map, they never decide participation), > `active-eidos.svelte.ts` (`scaling` / `scalingSource` / `getScaling()` / `data-scaling`). > Doc consolidated in [`theming/reference.md`](../theming/reference.md) §23. > `--density-scale` and `--density-content-scale` were removed in this sprint. > > **2026-07-06 — participation became model data.** Which families the zoom > multiplies is now DECLARED per element (`primitives.scaling`, partial maps > merge over the canonical defaults) instead of hardcoded in the emitters. > This also reconciles an unrecorded drift: commit `845d6579` (2026-06-22) > had made radius "scaling-responsive" against this RFC without updating any > doc — radius participation is now canonically `false` (chrome family), and > the original "as in Radix" justification below was corrected (Radix DOES > scale its radius; crispness is our own rationale). ## 0. Motivation `scaling` in Radix Themes (90/95/100/105/110%) is a **global zoom** that scales space, typography, heights and line-box with ONE factor — accessibility/zoom, not density. Eidos had a dead `--density-content-scale` (±4%) that tried to scale typography under the density axis, which **conflates density with zoom**: - **Density** (`compact` / `comfortable` / `spacious`): tightens layout (space + control height), **text stays stable** (legibility — Material/Carbon). Already done. - **Scaling** (this RFC): zoom — scales the px metrics **including typography**. The two axes are **orthogonal** and **compose** (you can have `compact` + `110%`). ## 1. Decisions - **Discrete**, Radix-class axis: `90 / 95 / 100 / 105 / 110` (%). Default `100`. Projected as `[data-scaling='90']` (blocks, cacheable) + `--scaling`. - **The axis is DEFINED by a per-element participation map** (user decision 2026-07-06): every orthogonal scale family declares whether the zoom multiplies it — `ScalingParticipationMap` (`config-types.ts`), canonical defaults in `STATIC_SCALING_PARTICIPATION` (`primitives/static.ts`). The generator derives ALL emission from the map; participation is model data (introspectable, guarded), never an emitter side effect. Canonical taxonomy: - **METRIC families scale** (what things occupy): `space`, `control-height`, `font-size`, `icon-size`, `blur` (blur joined 2026-06-15, §29). - **CHROME families stay crisp** (what draws the edges): `radius`, `border-width`, `shadow` — design-integer px under zoom. - Excluded outright: `line-height` (a **unitless ratio**; it already scales via font-size — multiplying it would **double** the effect), `z-index`, `opacity`, `motion` (durations/eases; `motion-distance` declares `false` in the map). - **Corrected reference note**: Radix Themes DOES scale its radius (`calc(base × scaling × radius-factor)`). Keeping radius crisp is OUR decision on our own rationale (chrome crispness + canonical axis semantics), not Radix parity. Radius keeps its own magnitude knob (`--radius-factor`) — roundness is themeable; zoom participation is a system-definition decision (config → regeneration), not a runtime theme knob. - Map boundaries: it governs DIRECT multiplication of raw px only — a value referencing a participating token (a gap on `var(--space-*)`) scales through the reference; `shadow` is declared for completeness but is structurally fixed (composite values can't wrap in one `calc()`). - **Composition with density** (multiplicative) on the 2 shared metrics: `--space-N: calc(base × var(--density-space-scale) × var(--scaling))`, `--control-height-N: calc(base × var(--density-control-scale) × var(--scaling))`. - **Prune** `--density-scale` (master) + `--density-content-scale`: they were the half-baked attempt at what this axis does properly. Reconciles §20.1 (density = stable text; scaling = zoom includes text). ## 2. Design **Primitive** (`lib/primitives/static.ts`): ```ts export const STATIC_SCALING = { '90': 0.9, '95': 0.95, '100': 1, '105': 1.05, '110': 1.1 } ``` Active default: `100`. **Types** (`config-types.ts`): `SCALING_KEYS = ['90','95','100','105','110']`, `ScalingKey`. The density primitive loses `scale` + `contentScale`. **Emission** (`render-css.ts`): - `:root` declares `--scaling-90: 0.9 … --scaling-110: 1.1` (constants) and `--scaling: var(--scaling-100)` (active). - Blocks `[data-scaling='90'] { --scaling: var(--scaling-90); }` … (the ≠100 ones). - `font-size` + `icon-size` move from verbatim to `calc(value × var(--scaling))`. - `space` + `control-height` add `× var(--scaling)` to the `calc` they already have. - `line-height`, `radius`, `border`, `shadow`, etc.: **unchanged**. **Runtime** (`active-eidos.svelte.ts`): new `scalingSource` (get/onChange, mirror of `densitySource`); `apply()` writes `data-scaling` on the host (next to `data-mode` / `data-density`); `dispose()` cleans it up. `EIDOS_SCALING_ATTR`. **Contract** (`contract.ts`): emits `--scaling` + the constants as knobs. ## 3. Conflicts (resolved) | Point | Resolution | |---|---| | Density × scaling on space/control-height | Multiplicative composition in the `calc`. Orthogonal in intent. | | line-height (ratio) | Do NOT scale — scales via font-size. | | radius/border/shadow/z/opacity/motion | Do NOT scale — chrome/structural families per the participation map. (The original "Radix parity" claim was wrong: Radix scales its radius; this is our own crispness decision.) | | Dead `--density-scale` + `content-scale` | Pruned — superseded by this axis. | | `size` prop | Orthogonal — `size` picks the tier, scaling multiplies the tier's value. Composes. | | Browser zoom | Composes multiplicatively (both px). App-level, distinct. | | §20.1 (typography doesn't scale with density) | Still true — density doesn't scale it; **scaling does**. Two axes. | ## 4. Implementation plan 1. `STATIC_SCALING` + remove `scale`/`contentScale` from `STATIC_DENSITY`. 2. Types (`SCALING_KEYS`/`ScalingKey`; adjust `DensityPrimitiveSet`). 3. `render-css`: scaling emission + blocks + `× var(--scaling)` on the 4 families; prune density scale/content-scale emission. 4. `ActiveEidos`: `scalingSource` + `data-scaling` + cleanup. 5. `contract`: scaling tokens; remove density scale/content-scale. 6. Regenerate `generated/base.css`. 7. Tests: update the shape of `--space-4`/`--control-height` (× scaling); scaled font-size test; `[data-scaling]` block; `scalingSource` test. 8. Verify (`check` + `vitest src/uix/eidos` + browser) + doc in theming reference.