Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Reference-grade guide to building a cross-platform type system — modular/fluid scales, leading & measure, pairing, variable fonts, OpenType, baseline rhythm, accessible ramps, and the tokens that ship it.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-08 | ✗→✓ | ▲ Improved | 319% | 0% |
| case-01 | ✓→✓ | = Same ✓ | 188% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 381% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 224% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 539% | 0% |
A type system is the set of rules that turns raw fonts into a coherent, scalable voice across every screen. Get it right and hierarchy, rhythm, and readability come for free; get it wrong and no amount of color or layout rescues it. This is the bible: anatomy → scale → spacing → selection → variable/OpenType → rhythm → hierarchy → tokens → accessibility → mistakes.
x. The perceived size of a font; two fonts at 16px look different sizes because of it. High x-height (Inter, Source Sans) reads larger and better at small sizes; low x-height (Garamond) feels elegant but needs more size.h, b, k) / below baseline (g, p, y). They eat into line-height; this is why a 16px font occupies ~22px of vertical space.o, e, a. Open counters survive small sizes and low resolution.c, e, a). Wide apertures (Frutiger, Inter) = legible at distance/small size; closed apertures hurt accessibility.1em = the font-size. The reference for all relative metrics.> Why care: you can't pick a UI font, set a scale, or align to a grid without knowing x-height, cap height, and how ascenders/descenders consume line-height.
Pick a base size and a ratio; multiply repeatedly to generate steps. This produces sizes that relate harmonically instead of arbitrary 14/15/17/19px noise.
Base size: 16px on web — it's the browser default, the minimum for comfortable body text, and the anchor for rem. Never set html { font-size: 14px } (it shrinks everyone's accessibility baseline).
Common ratios (smaller ratio = subtle, dense UI; larger = dramatic editorial):
| Ratio | Name | Feel | Use for | |------:|------|------|---------| | 1.125 | Major second | Tight, calm | Dense dashboards, data UI | | 1.200 | Minor third | Gentle | Product UI, forms | | 1.250 | Major third | Balanced (most popular) | General web/app UI | | 1.333 | Perfect fourth | Confident | Marketing + app hybrid | | 1.414 | Augmented fourth (√2) | Strong | Editorial | | 1.500 | Perfect fifth | Bold | Landing pages | | 1.618 | Golden ratio | Dramatic | Hero-driven sites |
Generating a scale (base 16, ratio 1.25): step_n = 16 × 1.25ⁿ → 10.24, 12.8, 16, 20, 25, 31.25, 39, 48.8, 61… Round to sane px, then store in rem.
css:root { --step--2: 0.64rem; /* 10px caption-min */ --step--1: 0.8rem; /* 13px caption */ --step-0: 1rem; /* 16px body */ --step-1: 1.25rem; /* 20px h5 */ --step-2: 1.5625rem; /* 25px h4 */ --step-3: 1.953rem; /* 31px h3 */ --step-4: 2.441rem; /* 39px h2 */ --step-5: 3.052rem; /* 49px h1 */ }
Pro move: use two ratios — a tight one (1.2) for body/UI steps, a wider one (1.333+) for display/headings. Headlines need more drama than paragraph text. A single ratio across the whole range either makes body cramped or headlines timid.
clamp()Fixed steps break between a phone and a 27" monitor. clamp(MIN, PREFERRED, MAX) scales type with the viewport, no media queries.
css/* fluid from 16px @360px viewport → 20px @1240px viewport */ --step-0: clamp(1rem, 0.93rem + 0.31vw, 1.25rem); --step-5: clamp(2.25rem, 1.1rem + 5.1vw, 4rem); /* hero */
The math: preferred = yIntercept + slope·vw, where slope = (maxPx − minPx) / (maxVw − minVw) and slope·100 → the vw value; the rem term is the intercept so it lands exactly at your min at the min viewport. Use a generator (utopia.fyi) rather than hand-tuning. Always keep rem in the preferred term so the value still responds to the user's browser zoom/root-size — a pure vw clamp defeats zoom and fails WCAG 1.4.4.
| Context | line-height (unitless) | |---|---| | Body text | 1.4–1.6 (1.5 safe default) | | Long-form reading | 1.5–1.7 | | UI labels / dense | 1.3–1.4 | | Headings (large) | 1.1–1.25 | | Display / hero | 0.95–1.1 (can go sub-1) |
Rules: set line-height unitless (1.5, not 24px) so it scales with each element's font-size. Bigger type needs less relative leading (the eye travels less to find the next line); small text needs more. Wider columns need more leading; narrow columns can use less. Tighten line-height as font-size grows — a single global 1.5 makes h1s look airy and broken.
max-width in ch (the 0 glyph width): max-width: 66ch;. Re-check per font — ch varies by typeface.-0.01em to -0.03em) — default spacing looks loose at big sizes.+0.05em to +0.1em) — caps have no ascender/descender cues, so they need air to be legible.em, never px, so tracking scales with size.AV, To, We). font-kerning: normal (default) uses the font's built-in pairs. text-rendering: optimizeLegibility enables optical kerning + ligatures but can hurt perf on huge text blocks — reserve for headings.Classification (so you can reason about pairing): Serif (Old-style/Garamond, Transitional/Times, Slab/Roboto Slab), Sans (Grotesque/Helvetica, Neo-grotesque/Inter, Humanist/Frutiger, Geometric/Futura), Mono (JetBrains Mono, SF Mono), Display/Script (headlines only).
Pairing principles:
System font stack (zero network cost, instant render, native feel):
cssfont-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; /* mono */ font-family: ui-monospace, "SF Mono", "Cascadia Code", "JetBrains Mono", Menlo, Consolas, monospace;
woff2 only for web fonts — ~30% smaller than woff, universal support. Don't serve ttf/otf to browsers.font-display:swap — render fallback immediately, swap in webfont when ready (FOUT). Default choice; text always visible.optional — use webfont only if it's near-instant (cached), else stick with fallback. Best for body text / Core Web Vitals (no layout shift).block (FOIT) — invisible text up to ~3s. Avoid; it hides content.preload the 1–2 critical fonts (body regular, maybe body bold): <link rel="preload" href="/inter.woff2" as="font" type="font/woff2" crossorigin>. Don't preload everything — it starves other resources.glyphhanger, fonttools) and use unicode-range to split by script — Latin-only subsets cut a font from ~200KB to ~15KB.size-adjust, ascent-override, descent-override on an @font-face fallback to eliminate layout shift).css@font-face { font-family: "Inter"; src: url("/fonts/inter-var.woff2") format("woff2-variations"); font-weight: 100 900; /* variable range */ font-display: swap; }
Variable fonts ship every weight/width in one file along a continuous axis, controlled at runtime. One ~70KB file replaces 6+ static weights.
Registered axes: wght (weight 1–1000), wdth (width %), opsz (optical size), ital (0/1), slnt (slant degrees). Plus font-specific custom axes (grade, etc.).
cssh1 { font-variation-settings: "wght" 720, "wdth" 105; } body { font-variation-settings: "wght" 400; }
Prefer the high-level properties (font-weight: 720) when they map to an axis — they're inherited and animatable; reach for font-variation-settings only for custom axes.
Optical sizing (opsz) adjusts letterforms for size: small text gets thicker strokes, more open spacing; large text gets refined, higher contrast. font-optical-sizing: auto (default) ties it to font-size automatically — keep it on. This is the single biggest quality jump variable fonts offer.
OpenType features (font-feature-settings, or semantic font-variant-*):
| Feature | Tag | Use | |---|---|---| | Ligatures | liga/dlig | On by default; discretionary for display only | | Tabular numerals | tnum | Tables, prices, anything that must align in columns | | Proportional numerals | pnum | Running prose | | Oldstyle numerals | onum | Numbers that flow with lowercase in body text | | Small caps | smcp | True small caps (never CSS font-variant: small-caps fakery if real ones exist) | | Fractions | frac | 1/2 → proper fraction | | Slashed zero | zero | Code, IDs, anywhere 0/O confusion matters | | Stylistic sets | ss01… | Alternate glyph designs (e.g. Inter's single-story a) |
css.data-table td { font-variant-numeric: tabular-nums; } /* prefer semantic */ code { font-feature-settings: "zero" 1, "ss01" 1; }
> Tabular vs proportional numerals is the classic system-design tell: numbers in tables, dashboards, and timers must be tnum so digits share one width and columns don't shimmer when values change. Body prose uses proportional for even color.
Consistent vertical spacing makes a page feel composed. Tie everything to a base unit — typically 4pt or 8pt. Margins, line-heights, and component spacing become multiples of it.
leading-trim/text-box-trim (shipping in modern browsers) finally lets you trim the half-leading above cap height and below baseline so components space by glyph, not by invisible line-box padding — adopt it where supported, with fallbacks.Hierarchy is built from five tools, in order of strength: size → weight → color/contrast → spacing → case. Use the fewest that work. Reaching for ALL-CAPS + bold + big + colored at once is shouting.
| Token | size (rem/px) | line-height | weight | tracking | use | |---|---|---|---|---|---| | display | clamp → 48–72 | 1.0 | 700 | -0.02em | hero | | h1 | 39 / 2.441rem | 1.1 | 700 | -0.01em | page title | | h2 | 31 / 1.953rem | 1.15 | 700 | -0.01em | section | | h3 | 25 / 1.563rem | 1.2 | 600 | 0 | subsection | | h4 | 20 / 1.25rem | 1.3 | 600 | 0 | card title | | body-lg | 18 / 1.125rem | 1.6 | 400 | 0 | lead paragraph | | body | 16 / 1rem | 1.5 | 400 | 0 | default | | body-sm | 14 / 0.875rem | 1.45 | 400 | 0 | secondary | | caption | 13 / 0.8rem | 1.4 | 400 | 0 | meta, timestamps | | overline | 12 / 0.75rem | 1.3 | 600 | +0.08em | UPPERCASE eyebrow | | code | 14 / 0.875rem | 1.5 | 400 | 0 | mono, tnum+zero |
Responsive ramps: don't scale every token equally. Headings should compress more than body on small screens (a 49px hero at 360px is unreadable; 16px body stays 16px). Fluid clamp() per token handles this; or define a mobile ramp that shrinks display/h1/h2 while body holds.
opsz for size-appropriate forms).> Map your single semantic token set (display, h1…caption, code) onto each platform's native ramp rather than inventing per-platform names. One mental model, native behavior everywhere.
rem, not px, for font-size. px font-size ignores the user's browser default; rem honors it. This is the #1 accessibility-via-typography rule.rem sizing + fluid layouts + clamp() with a rem term get you there; fixed vw-only type breaks zoom.| Don't | Do | Why | |---|---|---| | Justify text on the web (text-align: justify) | Left-align, ragged right | Browsers lack hyphenation/spacing control → ugly "rivers" of whitespace | | Use 4+ font families | 2–3 max (body, display, mono) | Each family is a download + a cognitive cost; more reads as chaos | | Set sizes in px | rem (+ clamp() for fluid) | px ignores user zoom/root-size → accessibility failure | | Single global line-height | Tighter for display, looser for body | Leading must scale inversely with size | | Run lines 100+ chars wide | max-width: 66ch | Eye loses the return line beyond ~75ch | | Faux bold/italic (font-weight:bold on a font with no bold file) | Load the real weight/italic, or use a variable font | Browser synthesizes by skewing/smearing → muddy, off-brand glyphs | | Track body text | Track only caps/display | Spacing breaks word shapes, hurting readability | | Hardcode iOS point sizes | Dynamic Type styles | Breaks the user's accessibility text scaling | | Tabular data with proportional nums | font-variant-numeric: tabular-nums | Columns shimmer/misalign as digits change width | | Thin weight (200/300) on small text | 400+ for body | Thin strokes vanish at small sizes / low DPI | | font-display: block on body | swap or optional | FOIT hides content for seconds |
Synthesis check before shipping a system: one base + one (or two) ratios → rem scale; tokens with size+line-height+weight+tracking each; ≤3 families in woff2 with font-display + preload + subsetting; tabular nums wired for data; fluid clamp() with rem terms; contrast + 200% zoom verified; mapped to native ramps on the platforms you target.
rem + clamp() + ch measure + variable woff2. Logical properties (margin-block, padding-inline) for i18n/RTL.sp units (which scale with user font setting — use sp for text, dp for layout).system-ui stacks for native feel and zero load cost; honor OS scaling.Other measured skills in the registry, with their headline benchmark lift.