Typography Fundamentals & System Architecture

Technical blueprint for engineering scalable, performant typography systems across modern web stacks. Establishes metric standards, loading protocols, and CWV optimization workflows for frontend engineering and design operations teams. Defines baseline calculations for Font Metrics & Baseline Alignment prior to architectural scaling. Typography architecture sits between two adjacent disciplines: the runtime mechanics of Font Loading & Delivery Strategies that govern when glyphs paint, and the measurement discipline of Font Performance Monitoring & Auditing that proves the system holds its CWV budget in the field.

The audience is frontend engineers, performance specialists, and design-systems maintainers who already ship @font-face rules and now need the typography layer to be predictable — same vertical rhythm, same baseline, same layout box whether the page renders in a zero-download system fallback or the loaded web font. The thesis of this guide is that typography is a metrics problem before it is an aesthetics problem: if you control units-per-em, ascent/descent, cap-height, and x-height, you control Cumulative Layout Shift, line-box height, and the modular scale that every component inherits. Get the metrics wrong and no amount of preloading hides the reflow.

Typography system architecture Three stacked layers — metric foundation, system layer, delivery — feed a shared Core Web Vitals budget gate checked by monitoring. Typography system architecture Metric foundation cap-height, x-height, overrides System layer tokens, type scale, system-ui Delivery layer preload, subset, swap CWV budget gate CLS<0.1, LCP<2.5s, INP<200ms Monitoring RUM + Lighthouse CI
Metric, system, and delivery layers all pass through one Core Web Vitals budget gate that field monitoring continuously verifies.

Core Web Vitals Impact: Where Typography Touches the Score

Typography is not a cosmetic concern for Core Web Vitals — it is one of the largest sources of Cumulative Layout Shift on text-heavy pages, and it directly gates Largest Contentful Paint whenever the largest element is text. The three field thresholds you are scored against (at the 75th percentile of real visitors) are LCP < 2.5s, CLS < 0.1, and INP < 200ms. Each has a distinct typographic failure mode.

CLS from metric mismatch is the headline risk. Cumulative Layout Shift is the sum of layout-shift scores, where each score is impact fraction × distance fraction. When the browser first paints with a fallback font and later swaps in the web font, every line of text re-lays-out if the two faces have different metrics. The driver is the ratio of glyph box heights and advance widths between fallback and web font. A worked example: suppose your fallback is Arial (units-per-em 2048, cap-height 1467, x-height 1062) and your web font is a tall-x-height face whose glyphs render ~12% taller per line. A 600px-tall article that reflows by 12% moves roughly 72px of content; on a 900px viewport that is an impact fraction near 0.67 and a distance fraction near 0.08, contributing a single shift around 0.05 — already half your entire CLS budget from one font swap. Multiply across a multi-column layout and you blow the budget on fonts alone.

The fix is metric matching: declare a fallback @font-face whose size-adjust, ascent-override, descent-override, and line-gap-override make the fallback occupy the same box as the web font, so the swap is invisible to layout. Done correctly, font-driven CLS drops to 0.00. This is the single most important reason the metric foundation sits at the base of the architecture diagram, and it is detailed in Font Metrics & Baseline Alignment and applied in Fallback Font Stack Design.

LCP tracks the hero font almost 1:1 when the largest contentful element is a heading or paragraph. Under font-display: block or auto, text cannot paint until the font is usable; under swap it paints in the fallback then repaints, and the repaint is the LCP candidate's final frame. Either way, the font's request-to-usable span (startTimeresponseEnd plus decode) bounds LCP. Preloading the critical weight via Preloading & Resource Hints is the lever; the typography layer's job is to ensure only the above-the-fold weight is on that critical path.

INP < 200ms is the quietest of the three. Decoding and shaping a large, un-subset font on the main thread can block an interaction's event handler, surfacing as a "Parse font" long task in the DevTools Performance flame chart. Variable fonts with many active axes amplify this because the shaper interpolates per axis. Keeping subsets under 50KB and limiting axes is the typography-side mitigation. The measurement playbook for all three lives in Font Performance Monitoring & Auditing.

Core Web Vital Threshold (p75) Typographic failure mode Primary mitigation
CLS < 0.1 Fallback↔web-font metric mismatch reflows every line size-adjust + ascent/descent overrides on fallback
LCP < 2.5s Hero text blocked on font fetch/decode Preload above-the-fold weight; subset
INP < 200ms Main-thread font parse/shape blocks input Subset < 50KB; limit variable axes

Architecture Overview: How the Layers Interconnect

The typography system is three layers stacked on one budget gate. The metric foundation (units-per-em, ascent, descent, line-gap, cap-height, x-height) defines the box every glyph occupies. The system layer turns those boxes into design decisions: the modular scale, the vertical-rhythm grid, the design tokens, and the System Font Stacks that render before any download. The delivery layer — preload, subset, font-display — decides when the web font replaces the fallback. All three are only "correct" if the swap moment passes the CWV budget gate, which field monitoring continuously re-verifies.

The central architectural decision most teams face is native (system-ui) vs web font, and static weights vs a variable font. There is no universal answer; it is a trade-off across payload, control, and CLS risk. The matrix below is the decision table.

Decision axis Option A Option B Choose A when Choose B when
Face source System-ui stack (zero download) Self-hosted web font UI chrome, dashboards, instant paint matters more than brand Brand/editorial identity requires a specific face
Weight strategy Static WOFF2 per weight Single variable font You ship 1–2 weights; want smallest critical bytes You ship 3+ weights/styles; need fluid wght
Axis strategy Fixed weights only Live wght/opsz axes Static design, no interactive type Optical sizing or animated weight is a real requirement
Fallback design Generic sans-serif Metric-matched @font-face fallback Prototype / non-critical text Production text where CLS must be 0
Scale strategy Fixed step ramp (media queries) clamp() fluid scale Few breakpoints, strict pixel control Continuous responsive scaling without query churn

The interconnection is causal: the metric foundation determines whether option B in the fallback design row can hit CLS 0; the system layer's scale strategy determines how clamp() tokens propagate; and the delivery layer is where the variable-vs-static decision turns into bytes on the wire. A single variable font only wins when it replaces 3+ static weights — below that, axis metadata overhead makes optimized static WOFF2 lighter, a trade-off explored under the loading blueprint's Variable Font Loading Techniques.

Typography Fundamentals: Metrics, Scale, and Rhythm

Everything downstream depends on four numbers and two systems built on them.

Font Metrics: the units that define the box

A font file declares its geometry in units-per-em (UPM) — the coordinate grid each glyph is drawn on. PostScript/CFF fonts conventionally use 1000 UPM; TrueType fonts commonly use 2048 (a power of two for hinting). Every other metric is expressed in these units and is scaled to the rendered font-size. The metrics that matter for layout are:

  • Ascent — distance from the baseline to the top of the line box, in font units. With 1000 UPM and an ascent of 950, ascent is 0.95em.
  • Descent — distance from the baseline down to the bottom of the line box (a positive magnitude below the baseline).
  • Line-gap — extra leading the font requests between lines; modern web typography usually overrides this to 0% and controls leading via line-height.
  • Cap-height — height of a flat-topped capital (H, I) from the baseline. It sets the visual weight of headings.
  • x-height — height of the lowercase x. Two fonts at the same font-size look radically different sizes if their x-heights differ by 15%, which is exactly why a naive font swap reflows: the rendered size changed even though font-size did not.

The reason ascent-override/descent-override/line-gap-override exist is to let you redefine these numbers on a fallback face so its line box matches the web font's line box. size-adjust scales every glyph's advance and height by a percentage, correcting x-height/cap-height mismatch in one declaration. Computing the right percentages is mechanical once you read both fonts' tables — see How to Calculate Cap-Height for Web Typography and the size-adjust derivation in Calculating size-adjust for system-ui fallback.

Font metric lines as fraction of em Bar chart of five font metrics expressed as a fraction of the em box: ascent, cap-height, x-height, baseline, and descent. Font metric lines as fraction of em Ascent 0.95em Cap-height 0.73em x-height 0.52em Descent 0.22em Line-gap 0.00em fraction of em
The web font and a metric-matched fallback must share ascent, cap-height, x-height, and descent for a swap to produce zero layout shift.

The modular scale

A modular scale sizes type by repeated multiplication of a base by a ratio, so steps relate harmonically instead of arbitrarily. With a 1rem base and a 1.25 (major third) ratio: 1rem → 1.25 → 1.563 → 1.953 → 2.441rem. Encoding the ratio once as a token means a single edit re-tunes the whole ramp, and components reference steps (--text-step-2) rather than pixels. Common ratios: 1.125 (major second, dense UI), 1.25 (major third, editorial), 1.333 (perfect fourth, expressive). Implementation patterns are in Type Scale & Modular Grids and the token recipe in Modular Scale with CSS Custom Properties.

Vertical rhythm

Vertical rhythm aligns text to a consistent baseline grid so paragraphs, headings, and lists share a common vertical cadence. The mechanism is unitless line-height (e.g. 1.5), which scales proportionally with inherited font-size — a unit-based line-height: 24px breaks the moment a nested element changes size. Choosing the leading multiple as a function of the grid (commonly a 4px or 8px base unit) keeps every line landing on the grid. The CSS lh unit now lets you express spacing in multiples of the current line height directly, which is the cleanest way to keep margins on-grid; that technique is covered in Line Height & Vertical Rhythm and specifically in Vertical Rhythm with the CSS lh Unit.

Optical sizing

Variable fonts with an opsz axis adjust glyph contrast and spacing for the rendered size — heavier optical sizes at display sizes, more open spacing at body sizes. font-optical-sizing: auto ties opsz to font-size automatically; manual control via font-variation-settings: 'opsz' <n> overrides it. This is the bridge between the metric foundation and the variable-font delivery decision, detailed in Optical Sizing & Variable Axes.

Edge Cases: Scripts, Zoom, and Media the Scale Must Survive

A typography system that only survives the happy path — one script, default zoom, screen media — is not production-ready. Four edge cases account for most field regressions that never show up in a designer's staging review.

Non-Latin scripts break the metric-matching assumption. size-adjust and the override triple are computed from one fallback face's metrics against one web font's metrics. The moment a paragraph mixes Latin with CJK, Arabic, or Devanagari, the browser's font-matching falls through to whatever font in the stack actually contains those glyphs — usually a locale system font with entirely different vertical metrics from the Latin fallback you tuned. The practical fix is a segmented stack: tune the Latin pair as described above, then append CJK- and emoji-specific faces after it rather than relying on one fallback chain to cover every script. unicode-range on each @font-face block lets the browser skip requesting faces whose ranges do not intersect the text, so the CJK face only downloads for pages that actually contain CJK text. See Fallback Stacks for CJK and Emoji Fonts for the full stack recipe.

User zoom and browser minimum font size can silently defeat a fixed scale. A modular scale built entirely from rem steps still respects the user's root font-size preference, but a scale expressed with raw px values or a clamp() preferred term that is vw-only does not track zoom correctly — WCAG 1.4.4 requires text to resize up to 200% without loss of content or function. Test the scale at both 200% browser zoom and with the OS-level minimum-font-size override (Chrome's chrome://settings/fonts "Minimum font size") set to 20px; a well-built clamp() scale with a rem floor continues to grow, while a vw-only scale can visually clip against a fixed-height container.

Print stylesheets need their own metric contract. Screen line-height tuned for a 16px body size at typical viewing distance is usually too loose for a printed page, and font-display has no effect once a page is rendered to a printer or PDF — printing captures whatever font state existed at print time, so a page printed mid-swap (before document.fonts.ready) bakes in the fallback font permanently. A defensive pattern is to defer enabling the browser's print dialog (or call window.print()) until document.fonts.ready has resolved, and to declare a leaner @media print block that increases contrast and reduces line-height for paper's higher effective resolution.

forced-colors mode and high-contrast overrides can change the effective box. Under forced-colors: active (Windows High Contrast, some OS-level accessibility modes), the browser may substitute system colors and, on some platforms, adjust font smoothing in ways that shift perceived x-height slightly even though the declared metrics are unchanged. This rarely produces a measurable CLS event, since the font itself is unchanged, but it is worth an explicit visual regression check — add a @media (forced-colors: active) rule that at minimum verifies your fallback stack still declares system-ui first, since forced-colors users are disproportionately relying on system rendering paths.

Migration case study results Four headline stats from a design-system migration: CLS before and after, font payload before and after. Migration case study results 0.14 Field CLS p75 before fix 0.01 Field CLS p75 after fix 310KB Static WOFF2 before 96KB Variable font after
Metric matching removed the shift; weight consolidation removed the bytes — both wins landed in the same migration.

Worked Example: Migrating a Design System to Metric-Matched Typography

Concrete numbers make the abstract metric work legible. Consider a mid-size marketing site shipping Inter at three static weights (400/500/700, ~310KB combined WOFF2) over a generic sans-serif fallback, measuring a field CLS p75 of 0.14 — entirely attributable to the font swap on first visit.

Step 1 — read both fonts' tables. ttx -t OS/2 -t hhea inter-400.woff2 and the equivalent for the fallback (Arial, extracted from a system font or approximated from published metrics) surface: Inter's UPM 2048, sTypoAscender 1968, sTypoDescender −494, x-height 1096; Arial's UPM 2048, ascent 1854, descent −434, x-height 1062.

Step 2 — compute size-adjust. Ratio of x-heights: 1096 ÷ 1062 ≈ 1.032, so size-adjust: 103.2% scales Arial's glyphs up by 3.2% to match Inter's rendered x-height at the same font-size.

Step 3 — compute the override triple against the adjusted fallback. Because size-adjust scales the whole em box, the ascent/descent overrides must be expressed relative to the unadjusted UPM before the size-adjust multiplier is applied by the engine — in practice this means deriving overrides empirically against the rendered result rather than by hand-arithmetic alone, then confirming visually. For this pair, ascent-override: 96%, descent-override: 24%, line-gap-override: 0% closed the remaining gap.

Step 4 — consolidate three static weights into one variable font. Since the site ships 3 weights, it crosses the guide's "3+ weights" threshold for variable-font consolidation: replacing 310KB of static WOFF2 with a single 96KB InterVariable subset (Latin + Latin-Extended, wght 400 700) both shrinks payload and lets buttons and headings interpolate weight live instead of stepping between fixed faces.

Step 5 — re-measure. After shipping the metric-matched fallback and the consolidated variable font, field CLS p75 dropped to 0.01 (residual shift from unrelated ad-slot reservation, not fonts), and total font transfer per page fell from 310KB to 96KB. LCP improved by 340ms because the single variable-font request replaced what had been three separate weight requests competing for bandwidth.

The generalizable lesson: metric matching removes the shift, and weight consolidation removes bytes — they are independent wins and both are usually available in the same migration.

Implementation Checklist

Work top-down: lock metrics, then the scale, then delivery, then verify the CWV gate. Each item produces a concrete artifact — a CSS rule, a token, or a DevTools check.

  1. Read the web font's head (UPM) and hhea/OS/2 tables (ascent, descent, line-gap, cap-height, x-height) with fonttools (ttx -t OS/2 -t hhea font.woff2).
  2. Pick a fallback face and compute size-adjust = (web-font x-height / fallback x-height) so x-heights match at the same font-size.
  3. Declare the fallback @font-face with size-adjust, ascent-override, descent-override, and line-gap-override: 0% so its line box equals the web font's.
  4. Define base size, ratio, and steps as CSS custom properties; reference steps in components, never raw pixels.
  5. Set body line-height unitless (≈1.5) and heading line-height unitless (≈1.2); express block margins in lh where supported.
  6. Add a metric-matched system-ui base stack so first paint is correct before any download.
  7. Preload only the above-the-fold weight with <link rel="preload" as="font" type="font/woff2" crossorigin>; set font-display: swap (or optional for non-critical text).
  8. For variable fonts, limit active axes to wght/opsz; register animated axes with @property to keep them off the main thread.
  9. Subset to the locale's unicode-range, target < 50KB per subset and < 150KB total per route.
  10. Verify CLS = 0 at the swap moment via DevTools → Rendering → Layout Shift Regions, then gate it in Lighthouse CI.
  11. Append script-specific fallback faces (CJK, emoji) after the tuned Latin pair, each scoped with its own unicode-range so unrelated locales never download them.
  12. Test the scale at 200% browser zoom and with the OS minimum-font-size override raised, to confirm rem-anchored steps still resize correctly.

Auditing & Monitoring the Typography Layer

A typography system is only "done" when its metrics are proven in the field, not just declared in CSS. Three checks catch the overwhelming majority of regressions.

CLS attribution at the swap moment. Record document.fonts.ready, then in a layout-shift observer flag any shift whose startTime falls within ~100ms of that mark — those are font-attributable. A correctly metric-matched fallback drives this to near zero. In the lab, DevTools → Rendering → Layout Shift Regions highlights the reflow visually, so you can watch (or fail to watch) text move when the font swaps. The full attribution workflow is in Debugging Font-Related Layout Shift.

Render-blocking and transfer timing. PerformanceResourceTiming exposes requestStart, responseEnd, encodedBodySize, and transferSize per font. Beacon responseEnd − requestStart to RUM and aggregate to p75 (target < 800ms for a critical weight). A preloaded-but-double-fetched font — two ResourceTiming entries for one URL — almost always means a missing crossorigin.

Budget gating in CI. Lighthouse CI asserts LCP/CLS thresholds and a per-font resource-size budget so a regression fails the build. Pin the throttle preset so lab numbers stay comparable to the field. The end-to-end instrumentation, from PerformanceObserver to lighthouserc.json, lives in Font Performance Monitoring & Auditing. Treat the lab gate as the leading indicator and CrUX p75 (the 28-day rolling field window) as the verdict.

Code Configuration Examples

Metric-matched fallback @font-face (eliminates swap CLS)

/* Web font: the brand face actually being loaded. */
@font-face {
  font-family: 'InterVariable';
  src: url('/fonts/inter-var.woff2') format('woff2-variations');
  font-weight: 100 900;
  font-display: swap;
}
/* Fallback: Arial reshaped to occupy Inter's exact line box.
   size-adjust matches x-height; overrides match the line box so the
   swap from this face to InterVariable produces zero layout shift. */
@font-face {
  font-family: 'Inter Fallback';
  src: local('Arial');
  size-adjust: 107.4%;
  ascent-override: 90%;
  descent-override: 22%;
  line-gap-override: 0%;
}
:root { --font-body: 'InterVariable', 'Inter Fallback', sans-serif; }
body { font-family: var(--font-body); }

Modular scale as design tokens

:root {
  --scale-ratio: 1.25;            /* major third */
  --text-step-0: 1rem;           /* base / body */
  --text-step-1: calc(var(--text-step-0) * var(--scale-ratio));
  --text-step-2: calc(var(--text-step-1) * var(--scale-ratio));
  --text-step-3: calc(var(--text-step-2) * var(--scale-ratio));
  --text-step--1: calc(var(--text-step-0) / var(--scale-ratio));
}
.lede  { font-size: var(--text-step-1); }
h2     { font-size: var(--text-step-2); }
h1     { font-size: var(--text-step-3); }
.caption { font-size: var(--text-step--1); }

clamp() fluid type scale (no media queries)

:root {
  /* clamp(min, preferred = base + viewport-relative growth, max) */
  --text-fluid-base: clamp(1rem, 0.9rem + 0.5vw, 1.25rem);
  --text-fluid-h1:   clamp(2rem, 1.4rem + 3vw, 3.5rem);
}
body { font-size: var(--text-fluid-base); line-height: 1.5; }
h1   { font-size: var(--text-fluid-h1);   line-height: 1.15; }

Cross-platform system-ui stack (zero-download first paint)

:root {
  --font-system:
    system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica,
    Arial, 'Apple Color Emoji', 'Segoe UI Emoji', sans-serif;
}
/* Dashboard chrome renders instantly with no font fetch. */
.app-shell { font-family: var(--font-system); }

Animated weight axis registered with @property (GPU-friendly)

@property --wght {
  syntax: '<number>';
  initial-value: 400;
  inherits: true;
}
.btn        { font-variation-settings: 'wght' var(--wght);
              transition: --wght 0.2s ease-out; }
.btn:hover  { --wght: 600; }
@media (prefers-reduced-motion: reduce) {
  .btn { transition: none; }
}

Segmented fallback stack for mixed-script paragraphs

/* Latin pair tuned per the worked example above, then CJK and
   emoji faces appended — each scoped so only relevant text
   triggers a download for that face. */
@font-face {
  font-family: 'CJK Fallback';
  src: local('Noto Sans CJK SC'), local('PingFang SC');
  unicode-range: U+4E00-9FFF, U+3400-4DBF;
}
:root {
  --font-body: 'InterVariable', 'Inter Fallback',
               'CJK Fallback', 'Apple Color Emoji',
               'Segoe UI Emoji', sans-serif;
}

Browser Support Matrix

These features underpin the metric and scale strategies above; confirm baselines before relying on them in production.

Feature Chromium Firefox Safari Notes
size-adjust (@font-face) 92+ 92+ 17+ Core lever for x-height matching
ascent-override / descent-override / line-gap-override 87+ 89+ 16.4+ Lock the fallback line box
lh / rlh units 109+ 120+ 16.4+ On-grid margins in line-height multiples
font-optical-sizing: auto 79+ 62+ 13.1+ Ties opsz to font-size
clamp() 79+ 75+ 13.1+ Fluid type without media queries
@property (registered custom props) 85+ 128+ 16.4+ Animatable font-variation-settings
Container queries (@container) 105+ 110+ 16.0+ Container-aware type scaling

For features with thinner support (notably lh units and @property in older Firefox), provide a static fallback declaration first and treat the modern rule as progressive enhancement. Safari's comparatively late size-adjust support (17+) means any site with a meaningful pre-17 Safari audience should verify the CLS budget separately for that cohort rather than assuming desktop Chrome numbers generalize — RUM segmented by browser, not just the aggregate p75, catches this.

Common Pitfalls

  • Shipping a web font with a generic sans-serif fallback. Without a metric-matched fallback @font-face, the swap reflows every line and contributes 0.05–0.15 to CLS — frequently the entire budget. Always pair the web font with a size-adjust-tuned fallback face.
  • Using unit-based line-height (e.g. 24px). It does not scale with inherited font-size, so nested components at different sizes break vertical rhythm. Use unitless values.
  • Hardcoding pixel font sizes in components. Bypasses the modular scale, so a ratio change can no longer re-tune the ramp and steps drift out of harmony. Reference scale tokens (--text-step-n) only.
  • Omitting crossorigin on a preloaded font. Triggers a duplicate fetch (the preload uses CORS mode, the CSS request does not), wasting the preload and delaying LCP. The attribute is required even same-origin.
  • Overloading a variable font with unused axes. Each active axis adds file size and per-glyph interpolation cost, inflating parse time and INP without visual benefit. Limit to wght/opsz.
  • Animating font-variation-settings without @property. An unregistered custom property forces synchronous main-thread layout on every frame, dropping frames; registering it with @property enables compositor animation.
  • Forgetting line-gap-override: 0%. Leaving the font's native line-gap in place on the fallback makes its line box taller than the web font's, reintroducing the very CLS the overrides were meant to remove.
  • Not testing the system-ui base before the web font loads. If the zero-download stack is not itself metric-aware, the page paints correct in the lab (warm cache) but shifts in the field on the very first, uncached visit — the one Google measures.
  • Tuning metrics against one script and shipping globally. A Latin-only fallback pair looks perfect in an English-language QA pass, then reflows on every CJK or Arabic page because those glyphs resolve to an untuned locale font. Ship a segmented stack per the edge-case section above.
  • Validating the scale only at 100% zoom. A clamp() expression with a vw-only preferred term, or a scale built from raw pixels, can pass every visual review at default zoom and still fail WCAG 1.4.4 at 200% zoom — test both explicitly before shipping.

Frequently Asked Questions

How do I make a font swap produce zero layout shift? Declare a fallback @font-face whose box matches the web font's. Set size-adjust to the ratio of x-heights (web-font x-height ÷ fallback x-height), then set ascent-override, descent-override, and line-gap-override: 0% so the fallback's line box equals the web font's. With the boxes identical, the browser repaints glyphs in place when the web font arrives and no line moves, so the font's CLS contribution is 0.00. Verify in DevTools → Rendering → Layout Shift Regions.

Should I use a variable font or static weights? Use a single variable font only when it replaces 3+ static weights or styles, or when you genuinely need fluid wght/opsz (animated weight, true optical sizing). Below three weights, the axis metadata and fvar table overhead make an optimized static WOFF2 set smaller on the critical path. Decide per route, not per site, and measure the actual bytes both ways.

Unitless or unit-based line-height? Unitless, always, for inheritable text. A unitless line-height: 1.5 is computed against each element's own font-size, so nested headings and body text keep proportional leading. A unit-based value (24px or even 1.5rem) is inherited as a fixed length and breaks rhythm wherever the size differs. Use the lh unit for margins you want expressed in line-height multiples, but keep the line-height property itself unitless.

When should I use a system-ui stack instead of a web font? For application chrome, dashboards, and dense UI where instant paint and zero CLS matter more than brand identity, a metric-aware system-ui stack renders immediately with no network cost and no swap. Reserve web fonts for editorial and brand surfaces where the specific typeface carries meaning — and even there, layer the web font on top of a metric-matched fallback so the system-ui paint is correct until it loads.

Why does my text reflow even though font-size did not change? Because two fonts at the same font-size can render at different visual sizes — their x-heights and cap-heights differ, and their ascent/descent define different line-box heights. The swap changes the rendered geometry, not the declared size, which is exactly what size-adjust and the ascent/descent overrides correct. The mismatch, not the font-size, is the source of the shift.

Does clamp() hurt accessibility or zoom? Not when the minimum is set in rem and the preferred term includes a rem component (e.g. clamp(1rem, 0.9rem + 0.5vw, 1.25rem)). A vw-only preferred value can defeat user zoom; mixing a rem floor with viewport growth keeps text responsive to both viewport size and the user's zoom/font-size preference, satisfying WCAG resize requirements.

How do I handle metric matching when a paragraph mixes scripts, like Latin and CJK? You cannot tune one fallback pair to cover every script; instead build a segmented stack where the tuned Latin pair comes first and script-specific fallback faces are appended after it, each scoped with its own unicode-range so a Latin-only page never downloads the CJK face. The residual CLS risk from the CJK segment is generally smaller in practice because paragraphs with mixed scripts usually reflow only the CJK runs, not the whole line, but it is still worth measuring separately per locale in RUM rather than assuming the Latin-tuned number applies everywhere.

What breaks a fixed pixel modular scale that a rem-based one survives? Two things: user zoom (which multiplies the root font size, so rem steps grow with it but px steps do not) and the OS-level minimum-font-size override some accessibility settings enable, which only affects computed sizes derived from relative units. A scale built entirely on --text-step-n tokens defined in rem inherits both behaviors correctly; one hardcoded in pixels silently stops resizing and can fail a WCAG audit even though it looks identical at default zoom.

Related