---
title: "Typography (craft)"
description: "Mobile check: display sizes land at 0.5–0.75× desktop (measured mobile H1 28–48px)."
canonical: https://void-design.vercel.app/docs/craft/typography
lastModified: 2026-09-16
---

# Typography

## 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.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, `&nbsp;` between number and unit, `×` for dimensions.
- **Visually hidden semantic H1** (hellohello pattern) is allowed for an art-directed poster hero: `<h1 className="sr-only">Acme — Design studio in Montevideo</h1>` 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:

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