Typography

Typography is the most impactful design decision you'll make. Text is 90%+ of most interfaces. Get typography right and a mediocre design looks polished. Get it wrong and an expensive design looks amateur.

Type Scale

A type scale is a set of predetermined font sizes that maintain visual harmony. Don't pick arbitrary sizes. Use a mathematical ratio.

Common Ratios

RatioNameScale (base 16px)Feel
1.125Major Second16, 18, 20, 23, 26Tight, subtle differences
1.200Minor Third16, 19, 23, 28, 33Moderate, most versatile
1.250Major Third16, 20, 25, 31, 39Clear, good for editorial
1.333Perfect Fourth16, 21, 28, 38, 50Dramatic, good for marketing
1.500Perfect Fifth16, 24, 36, 54, 81Very dramatic, headings dominate

Practical Type Scale

Most projects need 7-8 sizes:

:root {
  /* Using a ~1.25 (Major Third) scale with base 16px */
  --text-xs:      0.75rem;   /* 12px — Legal text, timestamps */
  --text-sm:      0.875rem;  /* 14px — Captions, metadata, labels */
  --text-base:    1rem;      /* 16px — Body text */
  --text-lg:      1.125rem;  /* 18px — Large body, card titles */
  --text-xl:      1.25rem;   /* 20px — H4, subheadings */
  --text-2xl:     1.5rem;    /* 24px — H3, section titles */
  --text-3xl:     2rem;      /* 32px — H2, page sections */
  --text-4xl:     2.5rem;    /* 40px — H1, page titles */
  --text-5xl:     3rem;      /* 48px — Display, hero sections */
  --text-6xl:     4rem;      /* 64px — Marketing headlines */
}

Applying the Scale

ElementSizeWeightColor
Display/Hero48-64pxBold (700-900)Primary or near-black
H1 (Page title)32-40pxBold (700)Near-black
H2 (Section)24-28pxSemi-bold (600)Near-black
H3 (Subsection)20-24pxSemi-bold (600)Near-black or dark gray
H4 (Card title)18-20pxMedium (500)Near-black or dark gray
Body16-18pxRegular (400)Dark gray (#333-#444)
Small/Caption14pxRegular (400)Medium gray (#666)
Micro/Legal12pxRegular (400)Light gray (#888)

Minimum body text: 16px on web. Never go smaller for primary reading text. On mobile, 16px prevents iOS Safari from zooming into form inputs.

Line Height (Leading)

Line height controls the vertical space between lines of text. Too tight and lines blur together. Too loose and the eye loses its place.

Text TypeLine HeightWhy
Large headings (32px+)1.1 - 1.2Tight because large text creates enough natural space
Small headings (20-28px)1.2 - 1.3Slightly more room as size decreases
Body text (16-18px)1.5 - 1.6Comfortable reading rhythm
Long-form reading1.6 - 1.8Extra room for sustained reading
Small text (12-14px)1.4 - 1.5Proportionally more space needed
h1 { font-size: 2.5rem; line-height: 1.15; }
h2 { font-size: 1.5rem; line-height: 1.25; }
h3 { font-size: 1.25rem; line-height: 1.3; }
p  { font-size: 1rem;    line-height: 1.6; }
small { font-size: 0.875rem; line-height: 1.5; }

Rule of thumb: As font size increases, line-height ratio decreases. As font size decreases, line-height ratio increases.

Line Length (Measure)

Optimal line length for comfortable reading is 50-75 characters per line (including spaces). Shorter than 40ch and the eye bounces too frequently. Longer than 85ch and the eye struggles to find the next line start.

/* Set line length using ch units (character width) */
.article-body {
  max-width: 65ch;   /* ~65 characters wide — ideal */
}

.wide-content {
  max-width: 75ch;   /* Acceptable maximum */
}

.narrow-column {
  max-width: 45ch;   /* Sidebar or card text */
}
ContextIdeal Widthch Value
Article/blog body55-70 characters55-70ch
Wide layout body65-80 characters65-80ch
Card/sidebar text30-45 characters30-45ch
Mobile full-widthWhatever the screen gives youControlled by padding
Centered hero text40-55 characters40-55ch

Letter Spacing (Tracking)

ContextLetter SpacingWhy
All-caps text+0.05em to +0.1emCaps lack ascenders/descenders, need space to breathe
Large headings (40px+)-0.01em to -0.02emTighten slightly for visual cohesion at large sizes
Body text0 (leave default)Font designers optimize for default tracking
Small text (12-14px)+0.01em to +0.02emSlight loosening improves legibility at small sizes
.all-caps-label {
  text-transform: uppercase;
  letter-spacing: 0.08em;   /* Open up all-caps text */
  font-size: 0.75rem;
  font-weight: 600;
}

.large-heading {
  font-size: 3rem;
  letter-spacing: -0.02em;  /* Tighten large display text */
}

Word Spacing

Generally leave word spacing at the default. Only adjust in specific cases:

SituationAdjustmentCSS
All-caps small textSlight increaseword-spacing: 0.05em
Justified text (avoid on web)Browser adjusts automaticallyNone
Monospace codeLeave defaultNone

Paragraph Spacing

Space between paragraphs should be larger than line spacing within paragraphs, so the reader's eye can clearly distinguish paragraph boundaries.

p {
  line-height: 1.6;          /* Space between lines */
  margin-bottom: 1.5em;      /* Space between paragraphs */
}

/* Alternatively use margin-top to avoid collapsing margin issues */
p + p {
  margin-top: 1.5em;
}

Rule: Paragraph spacing should be roughly 0.75× to 1.5× the font size. For 16px body text, 16-24px between paragraphs works well.

Font Pairing

Rules for Pairing

  1. Limit to 2 fonts. One for headings, one for body. Three is the absolute maximum for busy layouts.
  2. Create contrast. Pair a serif with a sans-serif, or a display font with a clean body font. Two similar fonts look like a mistake.
  3. Match x-heights. The fonts should feel proportional when placed next to each other at the same size.
  4. Ensure weight range. The heading font needs at least bold. The body font needs regular and bold.
  5. Test at actual sizes. A font that looks great at 48px may be illegible at 14px.

Proven Pairings

Heading FontBody FontStyle
Playfair Display (serif)Source Sans Pro (sans)Classic editorial
Montserrat (sans)Merriweather (serif)Modern professional
Inter (sans)Inter (sans)Clean tech/SaaS (single family)
Roboto Slab (slab serif)Roboto (sans)Related families, cohesive
Space Grotesk (sans)Space Mono (mono)Tech/developer brands
DM Sans (sans)DM Serif Display (serif)Matching design, modern

Single-Family Approach

Using one font family with multiple weights is the safest choice. These families work well alone:

FontWeights AvailableStyle
Inter100-900Clean, neutral, excellent for UI
DM Sans400, 500, 700Geometric, friendly
Source Sans Pro200-900Adobe's workhorse sans
IBM Plex Sans100-700Humanist, professional
Nunito200-900Rounded, approachable

Typography Best Practices

Color and Contrast

  • Never use pure black (#000000) on pure white (#FFFFFF). The contrast is too harsh for sustained reading. Use #1a1a1a to #333333 for body text on white backgrounds.
  • WCAG contrast minimums: 4.5:1 for normal text, 3:1 for large text (18pt+ or 14pt+ bold)
  • Use opacity or gray values to create text hierarchy:
.text-primary   { color: #1a1a1a; }  /* Headings, important text */
.text-secondary { color: #4a4a4a; }  /* Body text */
.text-tertiary  { color: #717171; }  /* Captions, metadata */
.text-disabled  { color: #a0a0a0; }  /* Disabled, placeholder — check contrast! */

Typographic Details

RuleBadGood
Use proper quotation marks"quote" (straight)"quote" (curly)
Use proper apostrophesdon't (straight)don't (curly)
Use en-dash for ranges5-10, Mon-Fri5–10, Mon–Fri
Use proper punctuation for breakstext - more textUse colons, commas, or periods instead of hyphens
Use ellipsis character... (three dots)… (single character)
Use multiplication sign1024x7681024×768

Readability Rules

  • Left-align body text. Justified text creates uneven word spacing ("rivers of white") on screens
  • Avoid all caps for text longer than 3-4 words. ALL CAPS IS HARDER TO READ BECAUSE ALL LETTERS HAVE THE SAME HEIGHT PROFILE
  • Limit italics. Useful for emphasis, book titles, and foreign words. Hard to read in long passages.
  • Don't underline non-links. On the web, underline universally signals "clickable"
  • Use bold for emphasis, not color. Color may not be visible to colorblind users
  • One space after periods. Two spaces is a typewriter convention that doesn't apply to digital type

Responsive Typography

Fluid Type with clamp()

/* Fluid scaling between viewport sizes */
h1 {
  font-size: clamp(2rem, 4vw + 1rem, 3.5rem);
  /* Minimum: 32px, Maximum: 56px, scales with viewport */
}

h2 {
  font-size: clamp(1.5rem, 3vw + 0.5rem, 2.25rem);
}

p {
  font-size: clamp(1rem, 1vw + 0.75rem, 1.125rem);
  /* Body stays between 16px and 18px */
}

Breakpoint-Based Scaling

If you prefer explicit control:

:root {
  --text-4xl: 2rem;     /* 32px on mobile */
  --text-base: 1rem;    /* 16px everywhere */
}

@media (min-width: 768px) {
  :root {
    --text-4xl: 2.5rem;  /* 40px on tablet */
  }
}

@media (min-width: 1200px) {
  :root {
    --text-4xl: 3rem;    /* 48px on desktop */
  }
}

Web Font Loading

Fonts are a performance concern. Unoptimized font loading causes either invisible text (FOIT) or a flash of unstyled text (FOUT).

/* Prefer swap to avoid invisible text */
@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter-var.woff2') format('woff2');
  font-weight: 100 900;
  font-display: swap; /* Show fallback immediately, swap when loaded */
}
StrategyBehaviorBest For
font-display: swapShow fallback, swap when loadedBody text (avoid invisible content)
font-display: optionalShow fallback, swap only if fastNon-critical fonts
font-display: blockHide text briefly, then showIcon fonts (prevents wrong glyphs)
System font stackNo loading at allMaximum performance

System Font Stack

For maximum performance, use the system font:

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
    Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
}

This uses each OS's native font: San Francisco on Mac/iOS, Segoe UI on Windows, Roboto on Android.

Common Mistakes

MistakeWhy It's WrongFix
Too many font sizesCreates visual noise, weakens hierarchyUse a type scale with 7-8 sizes max
Body text smaller than 16pxHard to read on mobile, triggers zoomSet body to 16px minimum
Not enough contrast between heading levelsHeadings blend with body textMake headings at least 1.5× body size or distinctly bolder
Using thin font weights for body textPoor readability, especially on low-res screensUse 400 (regular) minimum for body, 300+ only for large display text
Loading 6+ font weightsSlow page load, wasted bandwidthLoad only the weights you actually use (usually 2-3)
Line length over 85 charactersEye can't track back to the next lineSet max-width: 65ch on text containers
Same line-height for all sizesHeadings feel too loose, small text too tightDecrease line-height for large text, increase for small

Key Takeaways

  • Use a mathematical type scale (Major Third / 1.250 is the safest starting point).
  • Body text: 16px minimum, line-height 1.5-1.6, max-width 65ch.
  • Limit to 2 fonts maximum. One family with multiple weights is the safest approach.
  • Never use pure black on pure white. Use #1a1a1a for body text.
  • Use font-display: swap and load only the weights you need.
  • Apply clamp() for fluid typography that scales smoothly between breakpoints.
  • Tighten letter-spacing on large headings, loosen on small caps text.
  • Left-align body text. Always. No exceptions on the web.