Skip to content

Reference · craft

Typography

craft/references/typography.md111 linesupdated 16 Sept 2026

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-5xltext-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):

text
≤ 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.15em

Line-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

tsx
// 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-fg vs text-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 color brand-line, hover to currentColor.
  • 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:

    Acme — Design studio in Montevideo

    plus the visual lockup with 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:

text
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.