SuperAPI AI Directory

frontend-impl-typography-system

Frontend & websites

View repository

Updated 10-15 22:18

About this skill

Frontend Impl Typography System

This skill defines a deterministic rule set for shipping a typographic system : modular scale + fluid clamp() type, variable-font axes (preferring high-level shorthand over font-variation-settings), OpenType features via font-variant-* (with font-feature-settings reserved for stylistic sets), CLS-safe web-font loading (font-display: swap + metric-matched fallback), and text-wrap: balance|pretty (Baseline 2024). The skill builds on [[frontend-impl-design-tokens]] (token emission as CSS custom properties) and [[frontend-perf-core-web-vitals-inp]] (LCP / CLS budgets).

Sources : MDN: font-variation-settings (verified 2026-05-19), MDN: font-variant (verified 2026-05-19), MDN: font-feature-settings (verified 2026-05-19), MDN: @font-face/font-display (verified 2026-05-19), MDN: text-wrap (verified 2026-05-19), MDN: clamp() (verified 2026-05-19), MDN: @font-face/size-adjust (verified 2026-05-19), MDN: @font-face/ascent-override (verified 2026-05-19), web.dev: Preload critical assets (verified 2026-05-19).

Quick Reference

Modular scale ratios

Ratio Name Best for
1.125 Minor second Very dense UI (tables, dashboards). Steps barely distinguishable.
1.2 Minor third Compact app UI.
1.25 Major third General app UI. The most common default.
1.333 Perfect fourth Marketing-leaning products with mixed editorial sections.
1.414 Augmented fourth (sqrt 2) Paper-document-feel layouts.
1.5 Perfect fifth Editorial / long-form content.
1.618 Golden ratio Display / marketing landing pages with strong hierarchy.

Rule of thumb : ratio is LARGER for editorial, SMALLER for dense UI.

Fluid clamp() recipe

font-size: clamp(MIN_REM, BASE_REM + SLOPE_VW, MAX_REM);
Rule Detail
MIN, MAX in rem Honour the user's root font size.
Mix rem + vw in the preferred expression Pure vw collapses to unreadable on narrow viewports; pure rem is not fluid.
MAX >= 2 x MIN WCAG 1.4.4 Resize Text. Allows browser zoom to 200% without hitting the cap.
Verify at design min / max viewports If the preferred resolves outside [MIN, MAX], the value silently behaves like a constant.

A full step-indexed scale (ratio 1.2) :

:root {
  --step--2: clamp(0.69rem, 0.66rem + 0.18vw, 0.80rem);
  --step--1: clamp(0.83rem, 0.78rem + 0.28vw, 1.00rem);
  --step-0:  clamp(1.00rem, 0.93rem + 0.42vw, 1.25rem);
  --step-1:  clamp(1.20rem, 1.10rem + 0.62vw, 1.56rem);
  --step-2:  clamp(1.44rem, 1.30rem + 0.91vw, 1.95rem);
  --step-3:  clamp(1.73rem, 1.53rem + 1.32vw, 2.44rem);
  --step-4:  clamp(2.07rem, 1.80rem + 1.91vw, 3.05rem);
  --step-5:  clamp(2.49rem, 2.12rem + 2.76vw, 3.81rem);
}

Variable-font registered axes

Per MDN: font-variation-settings (verified 2026-05-19). ALWAYS use the shorthand for registered axes; reserve font-variation-settings for CUSTOM axes only.

Axis tag Use this shorthand Notes
"wght" font-weight: <number> 100..900 plus intermediate values supported by variable fonts.
"wdth" font-stretch: <percentage> Condensed (50%) to expanded (200%).
"slnt" font-style: oblique <angle> Slant angle in degrees.
"ital" font-style: italic Italic toggle (0/1).
"opsz" font-optical-sizing: auto Optical sizing matched to font-size.

CRITICAL RULE (per MDN) : "Font characteristics set using font-variation-settings will always override those set using the corresponding basic font properties, e.g., font-weight, no matter where they appear in the cascade." NEVER mix the two for the same axis ; pick one mechanism per axis project-wide. Prefer the shorthand.

font-variant-* over font-feature-settings

Per MDN: font-feature-settings (verified 2026-05-19) : "Whenever possible, Web authors should instead use the font-variant shorthand property or an associated longhand property... font-feature-settings shouldn't be used to enable small caps."

OpenType feature Use this font-variant-*
Small caps (smcp) font-variant-caps: small-caps
All small caps (c2sc, smcp) font-variant-caps: all-small-caps
Tabular numerals (tnum) font-variant-numeric: tabular-nums
Lining numerals (lnum) font-variant-numeric: lining-nums
Old-style numerals (onum) font-variant-numeric: oldstyle-nums
Slashed zero (zero) font-variant-numeric: slashed-zero
Diagonal fractions (frac) font-variant-numeric: diagonal-fractions
Common ligatures (liga) font-variant-ligatures: common-ligatures (default on)
Discretionary ligatures (dlig) font-variant-ligatures: discretionary-ligatures
Subscript / Superscript (subs / sups) font-variant-position: sub / font-variant-position: super

Reserve font-feature-settings for :

  • Stylistic sets : font-feature-settings: "ss03" 1;
  • Character-variant selectors : font-feature-settings: "cv11" 1;
  • Font-specific features that have NO font-variant-* keyword.

NEVER mix font-feature-settings and font-variant-* for the SAME feature ; behaviour is implementation-defined.

font-display value matrix

Per MDN: @font-face/font-display (verified 2026-05-19), Baseline since January 2020.

Value Block period Swap period Effect Use for
auto UA-defined UA-defined usually equivalent to block NEVER
block ~3 s infinite FOIT then FOUT-on-swap NEVER (kills LCP)
swap ~0 ms infinite FOUT immediate; webfont swaps in DEFAULT for hero / heading webfonts (paired with metric-matched fallback)
fallback ~100 ms ~3 s tiny invisible window, then fallback ; webfont only swaps if it arrives within 3 s Below-the-fold body when slight FOIT-glimpse is acceptable
optional ~100 ms 0 tiny invisible window, then fallback ; webfont only used if loaded almost instantly (cached) Above-the-fold body where stable LCP is the priority

Web-font preload (LCP-critical)

<link rel="preload" href="/fonts/Inter-roman.var.woff2" as="font" type="font/woff2" crossorigin>

Rules :

  1. crossorigin is MANDATORY (even for same-origin fonts) ; without it the browser fetches the font twice.
  2. as="font" is MANDATORY ; an incorrect as value triggers wrong priority and wastes the preload.
  3. Preload ONLY LCP-critical fonts. Preloading too many resources de-prioritises everything.
  4. Use woff2 only. WOFF1 / TTF / OTF have no place in 2026 evergreen.

text-wrap (Baseline 2024)

Per MDN: text-wrap (verified 2026-05-19).

Value Effect Use for
wrap (default) Greedy line-breaking. n/a
nowrap No wrap. Tags, badges, single-line UI.
balance Distributes characters equally across lines. Capped at 6 lines (Chromium) / 10 lines (Firefox) ; silently falls back beyond. Headings (h1-h3), captions, blockquote pull-quotes.
pretty Slower algorithm prioritising orphan minimisation and overall layout quality. Body copy where typography matters.
stable Keeps existing line breaks stable while content changes (typing in a contenteditable). [contenteditable].

CLS-safe fallback @font-face

@font-face {
  font-family: "Inter";
  src: url("/fonts/Inter-roman.var.woff2") format("woff2-variations");
  font-weight: 100 900;
  font-display: swap;
}

@font-face {
  font-family: "Inter-fallback";
  src: local("Arial");
  size-adjust: 107%;
  ascent-override: 90%;
  descent-override: 22.5%;
  line-gap-override: 0%;
}

body { font-family: "Inter", "Inter-fallback", system-ui, sans-serif; }
Descriptor Baseline Notes
size-adjust Baseline Widely Available (September 2023) Scales all glyph metrics by a percentage. LOAD-BEARING for CLS prevention.
ascent-override Limited Availability in 2026 Sets ascent above baseline. Progressive enhancement.
descent-override Limited Availability in 2026 Sets descent below baseline.
line-gap-override Limited Availability in 2026 Sets line-gap between lines.

size-adjust alone removes most font-swap CLS. The override descriptors close the remaining gap when supported.

Decision Trees

Decision : font-variant-* or font-feature-settings?

Does the OpenType feature have a font-variant-* keyword?

  Yes (small caps, tabular nums, common ligatures, fractions,
       slashed zero, subscript / superscript, oldstyle nums, etc.)
    -> ALWAYS font-variant-* (or the font-variant shorthand).
       NEVER raw font-feature-settings even though the OpenType tag
       would technically work.

  No (stylistic set ssNN, character variant cvNN, or any font-specific
      feature without a registered keyword)
    -> font-feature-settings: "ssNN" 1, "cvNN" 1;

Are both forms in the same stylesheet for the same feature?
  -> Pick one. Delete the other. Mixed forms have
     implementation-defined behaviour.

Decision : font-display: swap or optional (or anything else)?

Is this font on the page's LCP candidate text?

  Yes (h1 / hero text / above-the-fold large copy)
    -> font-display: swap.
       Preload with <link rel=preload as=font crossorigin>.
       Pair with metric-matched fallback @font-face to absorb FOUT.

  No, but the font is above-the-fold (nav, byline, body intro)
    -> font-display: optional. Browser uses webfont only if it
       arrives almost instantly (cached); otherwise the fallback
       stays for the whole session. LCP-friendly.

  No, the font is below-the-fold
    -> font-display: fallback. Small invisible window then fallback;
       webfont swaps in if it arrives within 3s.

  NEVER block (3-second FOIT) or auto (browser-dependent).

Decision : text-wrap: balance or pretty?

What kind of text is it?

  Heading (h1, h2, h3, h4) or short caption that is at most 6 lines.
    -> text-wrap: balance.
       Distributes characters equally; produces visually balanced
       multi-line titles instead of an orphan word.

  Body paragraph (p, li, dd) that may be many lines long.
    -> text-wrap: pretty.
       Optimises for orphan minimisation and overall flow.
       Skip if perf-critical on text-heavy pages.

  Editable area where the user is typing.
    -> text-wrap: stable.
       Keeps existing line breaks stable as the user types.

  Nav links, tags, single-line labels.
    -> text-wrap: nowrap (or leave default if wrap is desired).

Decision : variable font or static webfonts?

How many weights / styles do you ship?

  3 or more weights, or any custom axis, or you need oblique angles
  intermediate between regular and italic.
    -> Variable font (woff2-variations). One file covers all weights;
       use font-weight, font-stretch, font-style, font-optical-sizing
       for the registered axes; font-variation-settings ONLY for
       custom axes.

  1 or 2 weights, no custom axes.
    -> Static webfonts MAY be acceptable. But a typical variable
       font is 80-150 KB; six static fonts at 100 KB each is 600 KB.
       Variable usually wins.

How do you set the weight?
  Variable font -> font-weight: <number> shorthand.
  NEVER font-variation-settings: "wght" N (overrides shorthand
  silently regardless of cascade order).

Patterns

Pattern 1 : full type system with tokens

:root {
  --step--2: clamp(0.69rem, 0.66rem + 0.18vw, 0.80rem);
  --step--1: clamp(0.83rem, 0.78rem + 0.28vw, 1.00rem);
  --step-0:  clamp(1.00rem, 0.93rem + 0.42vw, 1.25rem);
  --step-1:  clamp(1.20rem, 1.10rem + 0.62vw, 1.56rem);
  --step-2:  clamp(1.44rem, 1.30rem + 0.91vw, 1.95rem);
  --step-3:  clamp(1.73rem, 1.53rem + 1.32vw, 2.44rem);
  --step-4:  clamp(2.07rem, 1.80rem + 1.91vw, 3.05rem);
  --line-tight: 1.2;
  --line-normal: 1.5;
  --line-loose: 1.75;
}

body { font-size: var(--step-0); line-height: var(--line-normal); font-optical-sizing: auto; }
h1   { font-size: var(--step-4); line-height: var(--line-tight); text-wrap: balance; }
h2   { font-size: var(--step-3); line-height: var(--line-tight); text-wrap: balance; }
h3   { font-size: var(--step-2); line-height: var(--line-tight); text-wrap: balance; }
p    { text-wrap: pretty; max-inline-size: 65ch; }
table { font-variant-numeric: tabular-nums; }

Pattern 2 : variable font with metric-matched fallback (no CLS)

@font-face {
  font-family: "Inter";
  src: url("/fonts/Inter-roman.var.woff2") format("woff2-variations");
  font-weight: 100 900;
  font-display: swap;
}

@font-face {
  font-family: "Inter-fallback";
  src: local("Arial");
  size-adjust: 107%;
  ascent-override: 90%;
  descent-override: 22.5%;
  line-gap-override: 0%;
}

:root { font-family: "Inter", "Inter-fallback", system-ui, sans-serif; }

Preload in <head> :

<link rel="preload" href="/fonts/Inter-roman.var.woff2" as="font" type="font/woff2" crossorigin>

Pattern 3 : variable-font custom axis

.expressive {
  font-weight: 600;
  font-optical-sizing: auto;
  font-variation-settings: "GRAD" 80, "YOPQ" 90;
}

Custom axes (uppercase 4-character tags) MAY use font-variation-settings ; registered axes (lowercase) MUST use their shorthand.

Anti-Patterns Index

See anti-patterns.md. Ten cataloged : line-height: 24px not unitless; font-display: block or auto; mixing font-weight with font-variation-settings: "wght" N; font-feature-settings: "smcp" 1 instead of font-variant-caps: small-caps; preload missing crossorigin; clamp() MAX < 2 x MIN; 6 static webfonts vs one variable; pure-vw font-size; text-wrap: balance on body paragraphs; no matched-fallback @font-face.

Reference Links

  • Methods and signatures : full property surface, OpenType tag map, axis table, font-display matrix, preload recipe, CLS metric-override descriptors.
  • Examples : renderable HTML demo with fluid scale + variable-font axis + font-variant-numeric live toggles ; nine additional patterns.
  • Anti-patterns : ten cataloged anti-patterns with symptom, root cause, and fix.

Cross-references

  • [[frontend-impl-design-tokens]] : token emission as CSS custom properties.
  • [[frontend-impl-responsive-layout-fluid]] : clamp() + viewport units that drive the fluid scale.
  • [[frontend-perf-core-web-vitals-inp]] : LCP and CLS budgets that constrain font loading choices.
  • [[frontend-syntax-css-color-modern]] : @property and modern CSS surface.
  • [[frontend-a11y-motion-contrast-wcag22]] : WCAG 1.4.4 Resize Text and minimum contrast.