1. The void type scale (from base.css)
| Utility | Size (360→1440px) | Line-height | Tracking | Use |
|---|---|---|---|---|
text-2xs |
11px | 16px | +0.01em | kbd, tiny meta (label-mono uses 11px) |
text-xs |
12px | 16px | +0.01em | captions, table meta, legal |
text-sm |
13px | 20px | +0.003em | UI workhorse: nav, buttons, labels, cards |
text-base |
15px | 24px | −0.006em | body default (product, dense marketing) |
text-md |
16px | 26px | −0.008em | marketing body paragraphs |
text-lg |
17→19px | 1.55 | −0.011em | lead / hero sub, long-form body |
text-xl |
19→22px | 1.4 | −0.014em | H3, card titles on marketing |
text-2xl |
22→27px | 1.3 | −0.017em | H3 large, pull quote |
text-3xl |
26→34px | 1.2 | −0.02em | H2 in docs/product |
text-4xl |
30→44px | 1.1 | −0.024em | H2 marketing (compact) |
text-display-sm |
34→54px | 1.04 | −0.028em × knob | H2 marketing |
text-display |
38→66px | 1.0 | −0.034em × knob | H1 product/docs |
text-display-lg |
42→80px | 0.96 | −0.04em × knob | H1 marketing |
text-display-xl |
46→96px | 0.92 | −0.045em × knob | poster hero, statements |
text-display* also set font-weight: var(--weight-display). text-5xl…text-9xl do not exist (the default scale is removed) and emit nothing.
Mobile check: display sizes land at 0.5–0.75× desktop (measured mobile H1 28–48px). If a word is longer than ~11 characters, test text-display-xl at 320px for overflow.
2. What the best sites measured (for judging overrides)
| Site | Display face | H1 px / lh / weight / tracking | Body | Workhorse |
|---|---|---|---|---|
| Linear | Inter Variable (cv01, ss03) | 64 / 1.00 / 510 / −0.022em | 16/24 | mono 14/24 |
| Vercel | Geist | 64 / 1.00 / 400 / −0.06em | 16/24 | 14/20 |
| Stripe | Söhne var (ss01) | 48 / 1.15 / 300 / −0.02em | 16 | 16/20 w300 |
| Resend | Domaine serif | 96 / 1.00 / 400 / −0.01em | Inter 16/24 | mono 12/16 |
| Granola | quadrant serif | 86 / 0.93 / 400 / −0.02em | 16/24 | 14 +0.02em |
| Attio | Inter Display | 64 / 0.95 / 600 / −0.02em | 16/22 −0.01em | 14 w500 |
| basement | Geist | 87 / 0.90 / 600 / −0.04em | 16/24 | — |
| hellohello | NHG Display Bold | 104 / 0.91 / 700 / −0.013em | 16/22.6 −0.04em | 16 w700 |
Takeaways: display lh 0.85–1.15; display tracking −0.01 to −0.06em (median −0.023); weights 300–600; body 16px/1.5; workhorse 13–14px; mono labels +0.1 to +0.15em.
Tracking by size (grotesk; serifs need about half):
≤ 13px +0.01 … +0.02em 14–16px 0 … −0.01em
18–24px −0.005 … −0.015em 32–40px −0.015 … −0.02em
48–64px −0.02 … −0.03em ≥ 72px −0.03 … −0.05em
uppercase / mono labels +0.05 … +0.15emLine-height by size: display 0.9–1.05; H2 1.0–1.15; H3 1.25–1.3; UI 1.33–1.43; body 1.5–1.65; long-form 1.6–1.75.
3. Pairings (all SIL OFL 1.1, on next/font/google unless noted)
| Pairing | Character | Direction |
|---|---|---|
| Geist + Geist Mono | engineered, neutral-modern | precision (default), terminal (mono only) |
| Inter (display cut via opsz) + JetBrains Mono | Linear-grade product | precision alt; enable "cv11","ss01" |
| Newsreader (opsz 6–72) + IBM Plex Mono | literary, trustworthy | editorial |
| Instrument Serif (≥ 40px only) + Instrument Sans | contemporary editorial | editorial alt; whole headline serif, never one word |
| Source Serif 4 + Source Sans 3 | institutional | editorial alt (research, gov) |
| Schibsted Grotesk + JetBrains Mono | sturdy news grotesk | swiss |
| Bricolage Grotesque (≥ 32px) + Figtree | warm, controlled quirk | warm |
| Fraunces (SOFT 50–100, WONK 0) + Commissioner | soft old-style | warm alt |
| IBM Plex Sans + IBM Plex Mono | technical, dense data | instrument |
Avoid as defaults (overused in generated sites): Inter for everything with no features, Poppins, Montserrat, Plus Jakarta Sans, Space Grotesk as body, Nunito. Fontshare fonts (Satoshi, General Sans, Cabinet, Clash) use the ITF Free Font License, which forbids subsetting: do not ship them in open-source repos. Berkeley Mono is not free for editors/terminals/OSS.
Budget: ≤ 2 families + mono, ≤ 4 files, variable woff2, subsets: ["latin"], display: "swap". Measured latin woff2: Geist 28.7 KB, Geist Mono 22.6, Inter 47.1, JetBrains Mono 39.5, Instrument Serif 20.5. Never request extra axes without measuring (Inter + opsz 47 → 71 KB; Bricolage all axes 40 → 128 KB). Monospace via next/font/local needs adjustFontFallback: false (see speed).
4. Rules with snippets
// Hero lockup: width in ch, balance from base layer, sub ≤ 2 lines
<h1 className="max-w-[16ch] text-display-lg text-fg">Deploy previews for every branch.</h1>
<p className="mt-6 max-w-[44ch] text-lg text-fg-muted">Push a branch, get a URL in 14 seconds.</p>
// Two-tone heading (Linear/Stripe): one element, second phrase muted
<h2 className="max-w-[24ch] text-display-sm text-fg">
Built for review. <span className="text-fg-muted">Every change gets a link, a diff and a rollback.</span>
</h2>
// Mono label voice
<p className="label-mono text-fg-muted">01 · Pipelines</p>
// Stat: tabular, unit smaller, label mono
<p className="text-display text-fg tabular-nums">14<span className="ml-1 text-2xl text-fg-muted">s</span></p>
<p className="label-mono mt-2 text-fg-subtle">median preview build</p>
// Long-form article column
<article className="mx-auto max-w-prose text-lg text-fg [&_p+p]:mt-6 [&_h2]:mt-16 [&_h2]:text-3xl">…</article>- OpenType: add per project in
globals.css, only for faces that have them:body { font-feature-settings: "cv11", "ss01"; }(Inter),"ss01"(Geist alternates). Numbers that change or align:tabular-nums; IDs/code:slashed-zero. - Italic: load italics only if used (
style: ["normal","italic"]costs a file). - Emphasis: by color (
text-fgvstext-fg-muted) and weight (font-medium), before size. Max 2 weights in UI text. - Uppercase: only via
label-mono(tracked). Never uppercase headings or buttons. - Links in prose: always underlined (
TextLink),underline-offset-4, decoration colorbrand-line, hover tocurrentColor. - Characters:
…“ ”’en dash–for ranges,between number and unit,×for dimensions. - Visually hidden semantic H1 (hellohello pattern) is allowed for an art-directed poster hero:
plus the visual lockup withAcme — Design studio in Montevideo
aria-hidden="true". Exactly one H1 per page (lint/multiple-h1).
5. Fluid overrides
Only for display sizes, always with a rem term (zoom-safe), max ≤ 2.5× min:
slope = (maxPx − minPx) / (maxVw − minVw); intercept = minPx − slope × minVw
font-size: clamp(min/16 rem, intercept/16 rem + slope×100 vw, max/16 rem)A poster hero bigger than text-display-xl (hellohello 7.24vw ≈ 104px at 1440): text-[clamp(3rem,1.2rem+6.2vw,6.75rem)] leading-[0.9] tracking-[-0.03em]. Test at 320px.