TL;DR
- Pair fonts by contrast: combine a serif with a sans-serif, or a display font with a neutral body font
- Limit yourself to 2 fonts maximum (3 is the absolute ceiling) for performance and cohesion
- Use a modular type scale (e.g., 1.25 ratio) for harmonious size relationships
- Fluid typography with clamp() gives smooth scaling without breakpoints
Font Pairing Principles
Font pairing isn't about finding two fonts that look similar — it's about finding two fonts that contrast while sharing an underlying harmony. Think of it like music: you want instruments that sound different but play in the same key.
Contrast, Not Conflict
The best pairings create clear visual hierarchy through contrast:
- Serif headings + sans-serif body — the classic. The serif adds personality to headings while the sans-serif stays clean for long reading (e.g., Playfair Display + Inter)
- Geometric headings + humanist body — geometric fonts (Poppins, Futura) feel modern; humanist fonts (Source Sans, Lato) feel warm for body text
- Display/decorative heading + neutral body — use a characterful display font for hero text but pair it with something invisible for body (e.g., Space Grotesk + system fonts)
What Makes Fonts Harmonize
Look for shared structural traits:
- Similar x-height — fonts with matching x-heights feel balanced at the same size
- Compatible proportions — if one font is wide, a narrow partner looks awkward
- Shared era or origin — fonts designed in the same period often pair naturally
Proven Font Pairings
/* Classic editorial: serif headings + sans body */
h1, h2, h3 {
font-family: 'Playfair Display', Georgia, serif;
font-weight: 700;
}
body {
font-family: 'Inter', -apple-system, sans-serif;
font-weight: 400;
}
/* Modern tech: geometric heading + clean body */
h1, h2, h3 {
font-family: 'Space Grotesk', sans-serif;
font-weight: 700;
}
body {
font-family: 'DM Sans', sans-serif;
font-weight: 400;
}
/* Elegant minimal: one font family, weight contrast */
h1, h2, h3 {
font-family: 'Inter', sans-serif;
font-weight: 800;
letter-spacing: -0.02em;
}
body {
font-family: 'Inter', sans-serif;
font-weight: 400;
}
Building a Type Scale
A type scale creates mathematical relationships between font sizes. Pick a base size and a ratio:
- 1.200 (Minor Third) — subtle, conservative
- 1.250 (Major Third) — balanced, most popular
- 1.333 (Perfect Fourth) — clear hierarchy
- 1.500 (Perfect Fifth) — dramatic, editorial
:root {
--text-xs: 0.64rem; /* 10.24px */
--text-sm: 0.8rem; /* 12.8px */
--text-base: 1rem; /* 16px — base */
--text-lg: 1.25rem; /* 20px — × 1.25 */
--text-xl: 1.563rem; /* 25px — × 1.25² */
--text-2xl: 1.953rem; /* 31.25px — × 1.25³ */
--text-3xl: 2.441rem; /* 39px — × 1.25⁴ */
--text-4xl: 3.052rem; /* 48.8px — × 1.25⁵ */
}
Fluid Typography
Static font sizes force you into breakpoint-based overrides. clamp() lets text scale fluidly:
:root {
/* clamp(min, preferred, max) */
--text-base: clamp(1rem, 0.95rem + 0.25vw, 1.125rem);
--text-xl: clamp(1.25rem, 1rem + 0.8vw, 1.75rem);
--text-3xl: clamp(2rem, 1.2rem + 2vw, 3.5rem);
}
/* Apply with line-height adjustments */
h1 {
font-size: var(--text-3xl);
line-height: 1.1;
}
h2 {
font-size: var(--text-xl);
line-height: 1.2;
}
p {
font-size: var(--text-base);
line-height: 1.6;
}
💡 Use the Fluid Type Calculator to generate clamp() values, or the PX↔REM Converter for quick unit conversions.
Line Height and Measure
Two often-overlooked settings that dramatically affect readability:
- Line height: 1.4-1.6 for body text, 1.1-1.3 for headings. Tighter for larger text.
- Measure (line length): 45-75 characters per line is the sweet spot. Use max-width: 65ch on text containers.
.prose {
max-width: 65ch;
font-size: var(--text-base);
line-height: 1.6;
}
.prose h2 {
line-height: 1.2;
margin-top: 2em;
margin-bottom: 0.5em;
}
Performance: Loading Fonts
/* Preload critical fonts in HTML <head> */
<link rel="preload" href="/fonts/inter-var.woff2"
as="font" type="font/woff2" crossorigin>
/* Use font-display: swap to prevent invisible text */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-var.woff2') format('woff2');
font-weight: 100 900;
font-display: swap;
}
/* System font stack fallback */
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont,
'Segoe UI', Roboto, sans-serif;
}
Best Practices
- Do limit yourself to 2 typefaces — weight and size variation within a family creates enough hierarchy
- Do use variable fonts — one file, every weight. Massive performance win
- Do set max-width: 65ch on text blocks for optimal reading comfort
- Do adjust line-height based on font size — tighter for headings, looser for body
- Don't load 6+ font weights if you only use 2-3 — each adds ~15-30KB
- Don't use decorative fonts for body text — they kill readability
- Don't set font sizes in px — use rem for accessibility (respects user font size preferences)
Common Mistakes
- All-caps body text — fine for short labels, painful for paragraphs
- Too many font sizes — if you have 15 different sizes, you don't have a scale. Pick 6-8 and stick to them
- Ignoring fallback fonts — if your web font fails to load, the fallback should have similar metrics to avoid layout shift
- Letter-spacing on body text — body text is designed to be read at default spacing. Only adjust on headings and all-caps text
- Not testing with real content — "Lorem ipsum" always looks fine. Test with actual headlines and paragraphs
Tools to Explore
- Font Explorer — browse and preview font pairings
- Fluid Type Calculator — generate fluid typography scales
- PX↔REM Converter — convert between px and rem
- Clamp Generator — build responsive clamp() values