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
| Ratio | Name | Scale (base 16px) | Feel |
|---|---|---|---|
| 1.125 | Major Second | 16, 18, 20, 23, 26 | Tight, subtle differences |
| 1.200 | Minor Third | 16, 19, 23, 28, 33 | Moderate, most versatile |
| 1.250 | Major Third | 16, 20, 25, 31, 39 | Clear, good for editorial |
| 1.333 | Perfect Fourth | 16, 21, 28, 38, 50 | Dramatic, good for marketing |
| 1.500 | Perfect Fifth | 16, 24, 36, 54, 81 | Very 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
| Element | Size | Weight | Color |
|---|---|---|---|
| Display/Hero | 48-64px | Bold (700-900) | Primary or near-black |
| H1 (Page title) | 32-40px | Bold (700) | Near-black |
| H2 (Section) | 24-28px | Semi-bold (600) | Near-black |
| H3 (Subsection) | 20-24px | Semi-bold (600) | Near-black or dark gray |
| H4 (Card title) | 18-20px | Medium (500) | Near-black or dark gray |
| Body | 16-18px | Regular (400) | Dark gray (#333-#444) |
| Small/Caption | 14px | Regular (400) | Medium gray (#666) |
| Micro/Legal | 12px | Regular (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 Type | Line Height | Why |
|---|---|---|
| Large headings (32px+) | 1.1 - 1.2 | Tight because large text creates enough natural space |
| Small headings (20-28px) | 1.2 - 1.3 | Slightly more room as size decreases |
| Body text (16-18px) | 1.5 - 1.6 | Comfortable reading rhythm |
| Long-form reading | 1.6 - 1.8 | Extra room for sustained reading |
| Small text (12-14px) | 1.4 - 1.5 | Proportionally 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 */
}
| Context | Ideal Width | ch Value |
|---|---|---|
| Article/blog body | 55-70 characters | 55-70ch |
| Wide layout body | 65-80 characters | 65-80ch |
| Card/sidebar text | 30-45 characters | 30-45ch |
| Mobile full-width | Whatever the screen gives you | Controlled by padding |
| Centered hero text | 40-55 characters | 40-55ch |
Letter Spacing (Tracking)
| Context | Letter Spacing | Why |
|---|---|---|
| All-caps text | +0.05em to +0.1em | Caps lack ascenders/descenders, need space to breathe |
| Large headings (40px+) | -0.01em to -0.02em | Tighten slightly for visual cohesion at large sizes |
| Body text | 0 (leave default) | Font designers optimize for default tracking |
| Small text (12-14px) | +0.01em to +0.02em | Slight 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:
| Situation | Adjustment | CSS |
|---|---|---|
| All-caps small text | Slight increase | word-spacing: 0.05em |
| Justified text (avoid on web) | Browser adjusts automatically | None |
| Monospace code | Leave default | None |
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
- Limit to 2 fonts. One for headings, one for body. Three is the absolute maximum for busy layouts.
- 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.
- Match x-heights. The fonts should feel proportional when placed next to each other at the same size.
- Ensure weight range. The heading font needs at least bold. The body font needs regular and bold.
- Test at actual sizes. A font that looks great at 48px may be illegible at 14px.
Proven Pairings
| Heading Font | Body Font | Style |
|---|---|---|
| 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:
| Font | Weights Available | Style |
|---|---|---|
| Inter | 100-900 | Clean, neutral, excellent for UI |
| DM Sans | 400, 500, 700 | Geometric, friendly |
| Source Sans Pro | 200-900 | Adobe's workhorse sans |
| IBM Plex Sans | 100-700 | Humanist, professional |
| Nunito | 200-900 | Rounded, 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
| Rule | Bad | Good |
|---|---|---|
| Use proper quotation marks | "quote" (straight) | "quote" (curly) |
| Use proper apostrophes | don't (straight) | don't (curly) |
| Use en-dash for ranges | 5-10, Mon-Fri | 5–10, Mon–Fri |
| Use proper punctuation for breaks | text - more text | Use colons, commas, or periods instead of hyphens |
| Use ellipsis character | ... (three dots) | … (single character) |
| Use multiplication sign | 1024x768 | 1024×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 */
}
| Strategy | Behavior | Best For |
|---|---|---|
font-display: swap | Show fallback, swap when loaded | Body text (avoid invisible content) |
font-display: optional | Show fallback, swap only if fast | Non-critical fonts |
font-display: block | Hide text briefly, then show | Icon fonts (prevents wrong glyphs) |
| System font stack | No loading at all | Maximum 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
| Mistake | Why It's Wrong | Fix |
|---|---|---|
| Too many font sizes | Creates visual noise, weakens hierarchy | Use a type scale with 7-8 sizes max |
| Body text smaller than 16px | Hard to read on mobile, triggers zoom | Set body to 16px minimum |
| Not enough contrast between heading levels | Headings blend with body text | Make headings at least 1.5× body size or distinctly bolder |
| Using thin font weights for body text | Poor readability, especially on low-res screens | Use 400 (regular) minimum for body, 300+ only for large display text |
| Loading 6+ font weights | Slow page load, wasted bandwidth | Load only the weights you actually use (usually 2-3) |
| Line length over 85 characters | Eye can't track back to the next line | Set max-width: 65ch on text containers |
| Same line-height for all sizes | Headings feel too loose, small text too tight | Decrease 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: swapand 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.