zudo-css-wisdom
GitHub repository

Type to search...

to open search from anywhere

Stage-Proportional Typography

Pure-cqi type scales for slides and other fixed-composition stages — when to break the rem-dominant rule.

The Problem

A slide, an embedded presentation stage, a fixed-aspect preview card — these are fixed-composition surfaces: the design is authored at one canonical size (say 1280×720), and every rendering is meant to be a faithful miniature or enlargement of that composition. Text at 6.85% of the stage width should stay 6.85% of the stage width at every size, the way PowerPoint, reveal.js, and Marp behave.

The standard fluid-type guidance — additive clamp() tokens with a dominant rem term (see Container Queries → Keep the rem Term Dominant) — actively breaks this. The rem floor stops text from shrinking once the stage drops below a fraction of the design width, so on a narrow stage the type balloons relative to its canvas, wraps, collides with the footer, and gets clipped by the stage's overflow: hidden.

Measured on a real slide system with rem-dominant tokens (title designed at 6.85% of stage width):

Stage widthTitle as % of stage width
1280px (design width)6.85% ✓
640px11.1%
320px19.6% — roughly 3× too big, content clipped

AI agents (and the rem-dominant rule itself) push toward additive tokens everywhere. For a stage, that is the wrong default.

The Solution

Inside a fixed-composition stage, make the type scale purely proportional to the stage: each token is a plain cqi value, derived from the design-width pixel size.

preferred-cqi = design-px / (design-width-px / 100)

For a 1280px design width, 1cqi = 12.8px:

.slide-stage {
  container-type: inline-size;

  /* 25.6px at the 1280px design width -> 2cqi, at EVERY width */
  --type-base:    max(0.3rem, 2cqi);
  --type-lg:      max(0.396rem, 2.6375cqi);
  --type-display: max(1.028rem, 6.85cqi);   /* 87.68px @ 1280 */
}

Two deliberate details:

  • No max cap. A projection-sized stage should keep scaling up — proportionality is the contract in both directions.

  • A tiny max() legibility floor, not a design floor. The floor exists only so text stays renderable in degenerate containers; it must be far below any size the design targets.

The Uniform-Floor Technique

If each token gets an arbitrary floor, the floors engage at different stage widths, and the type hierarchy collapses unevenly (body text freezes while the display size is still shrinking, or vice versa). Instead, derive every floor from the same minimum stage width:

floor-rem = preferred-cqi × (min-stage-px / 100) / 16

With min-stage-px = 240, every token's floor is its proportional value at a 240px stage — all floors engage simultaneously, so relative scale between tokens is preserved all the way down, and below 240px the whole scale freezes together. Below that point the stage is thumbnail territory, which is the embedder's problem (scale the thumbnail, don't restyle the type).

The Zoom Trade-off — Decide It Consciously

The rem-dominant rule exists for WCAG 1.4.4: browser text-only zoom scales rem, not cqi, so pure-cqi type ignores it. And don't assume full-page zoom fixes this either — on a viewport-fitted stage, zooming shrinks the CSS-px viewport, the stage shrinks with it, and the cqi-derived text stays at roughly the same physical size (the floor only changes that once the stage hits the floor threshold). Full-page zoom genuinely enlarges the type only when the stage has a fixed CSS-px size rather than fitting the viewport. Going proportional therefore means accepting an accessibility trade-off, not routing around it — the same trade-off reveal.js, Marp, and Google Slides make for fixed-composition surfaces.

Write this decision down (in the token file's comment and the architecture doc), and if a test asserted the old contract, invert it — otherwise the next contributor "fixes" the tokens back.

StrategyProportional to stage?Responds to text-only zoom?Notes
Additive rem-dominant clamp()✗ below ~⅓ design widthCorrect for cards/widgets in flowing pages — the default rule
Pure cqi + uniform floor✗ (floor only)Correct for fixed-composition stages
Stage font-size: Ncqi + em scale✓ until the single shared floorOne knob, but one floor breaks all sizes at once
transform: scale() of a fixed-px canvas✓ (everything, incl. hairlines)Abandons DOM-native sizing; text can blur
rem-dominant vs pure-cqi type in a slide stage — drag the stage narrower

Verifying Proportionality Deterministically

"Looks about right" is how the rem floor sneaks back in. The invariant is checkable: rendered font-size ÷ stage width is a constant. Assert it at two or more widths (kept above the uniform-floor width so floors are inactive):

// Playwright: title designed at 6.85% of stage width
for (const width of [1600, 480]) {
  await page.setViewportSize({ width, height: 900 });
  const stage = (await page.locator('.slide-stage').boundingBox())!.width;
  const font = await page
    .locator('.slide-title')
    .evaluate((el) => parseFloat(getComputedStyle(el).fontSize));
  expect(font / stage).toBeCloseTo(0.0685, 3);
}

The mirror-image test matters too: if the codebase previously asserted the rem-dominant contract ("font shrinks less than the container"), that test must be inverted in the same commit as the token change, or CI blocks the migration.

Common AI Mistakes

  • Applying the rem-dominant rule inside a stage: the ~60% rem-share rule is for components in flowing pages. Inside a fixed-composition stage it produces text that is 2–3× oversized on small stages, with content clipped by overflow: hidden.

  • Applying stage-proportional tokens outside the stage: the inverse mistake. Page chrome, toolbars, and prose keep additive/rem sizing — pure cqi is scoped to the stage subtree only.

  • Per-token arbitrary floors: floors that engage at different stage widths collapse the type hierarchy unevenly. Derive every floor from one minimum stage width.

  • Adding a max cap out of habit: caps re-break proportionality on large stages. A stage scales in both directions.

  • Changing the tokens but not the contract artifacts: the architecture doc, the token-file comment, and any test asserting the old behavior must flip in the same change.

  • Migrating with calc() conversion left implicit: derive each cqi coefficient from the design-width px value (px ÷ (design-width ÷ 100)) so the design-width rendering is bit-identical before and after.

When to Use

  • Slide systems, presentation stages, deck viewers and their thumbnails

  • Fixed-aspect embeds authored at a canonical design size (video-overlay lower-thirds, certificate/receipt previews, social-card renderers)

  • Any surface where the honest mental model is "a scaled image of one composition"

Stay with additive rem-dominant tokens (the Container Queries default) for:

  • Cards, widgets, and components that reflow rather than scale

  • Anything in a flowing document where text-only zoom accessibility is the priority

  • Body text in prose contexts

References

Revision History

CreatedUpdated