Skip to content
Design

Typography & Font Pairing

A developer's guide to picking and combining fonts that actually work together.

stellae.design

8 min read

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

css
/* 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
css
: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:

css
: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.
css
.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

html
/* 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
Designdesign

Was this article helpful?