Letter Spacing and Word Spacing
Fine-tuning the space between characters and words can transform the feel of text from generic to refined.
/* ── Letter spacing (tracking) ───────────────────────────────────────── */
/* ❌ Using px for letter-spacing — doesn't scale with font size */h1 { letter-spacing: -2px;}
/* ✅ Using em — scales proportionally with font size *//* Uppercase labels: open up the tracking significantly */.label-uppercase { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.1em; /* 10% of font size — more air for small caps */ font-weight: 600;}
/* Display headings: slightly negative tracking for large text */h1 { font-size: clamp(3rem, 5vw, 6rem); letter-spacing: -0.025em; /* -2.5% — large text needs optical tightening */}
h2 { font-size: clamp(2rem, 3vw, 3.5rem); letter-spacing: -0.015em; /* slightly less tight than h1 */}
/* Body text: near-zero or zero letter-spacing */body { font-size: 1rem; letter-spacing: 0.01em; /* just a hair of extra space — often improves readability */}
/* ── Word spacing ─────────────────────────────────────────────────────── */
/* Rarely needed with modern typefaces, but useful for specific effects */.pull-quote { font-size: 1.5rem; word-spacing: 0.05em; /* 5% extra space between words */}
/* ── A complete refined heading style ───────────────────────────────────*/.hero-title { font-size: clamp(3rem, 6vw, 7rem); font-weight: 800; line-height: 1.05; letter-spacing: -0.03em; /* tight tracking for very large display type */ text-wrap: balance; /* distribute words evenly across lines */}text-wrap: balance and pretty — New CSS Superpowers
Two relatively new CSS values completely change how text wraps across lines, eliminating the common problem of a single orphaned word on the last line.
/* ── text-wrap: balance ─────────────────────────────────────────────── *//* BEFORE: "The most important thing in communication is typography"
AFTER (with balance): "The most important thing in communication is typography"
balance: distributes words as evenly as possible across lines Best for: headings, subheadings, short display text (max ~6 lines)*/
h1,h2,h3,h4,h5,h6,.card-title,.caption,.label { text-wrap: balance;}
/* ── text-wrap: pretty ───────────────────────────────────────────────── *//* pretty: eliminates orphans (single words alone on the last line) Specifically designed for body text — better for long paragraphs than balance, which has a performance cost at scale.*/p,li,blockquote { text-wrap: pretty;}
/* ── Practical complete example ─────────────────────────────────────── */.blog-post h2 { font-size: clamp(1.75rem, 3vw, 2.5rem); line-height: 1.2; letter-spacing: -0.02em; text-wrap: balance; /* No more awkward single-word last lines in headings */ max-width: 30ch; /* Encourage natural 3-4 line wrapping */}
.blog-post p { font-size: clamp(1rem, 1.5vw + 0.5rem, 1.125rem); line-height: 1.7; max-width: 70ch; text-wrap: pretty; /* Clean, natural wrapping without orphans */}Font Loading and Performance
No matter how good your type scale is, if fonts load slowly, users see a flash of unstyled text (FOUT) or invisible text (FOIT). Both are jarring.
/* ── @font-face: loading custom fonts ───────────────────────────────── */
@font-face { font-family: 'Inter'; /* Provide both woff2 and woff for maximum compatibility */ /* woff2 is smaller and more compressed — browsers that support it will use it */ src: url('/fonts/inter-variable.woff2') format('woff2-variations'), url('/fonts/inter-variable.woff') format('woff-variations'); font-weight: 100 900; /* For variable fonts: declare the full range */ font-style: normal; /* font-display: swap - Shows system font immediately (no invisible text) - Swaps to custom font when it loads (brief re-render) - Best for body text: content is visible right away
font-display: optional - Shows custom font only if it loads within a very short window - Falls back to system font permanently if it misses the window - Best for non-essential display fonts: no layout shift
font-display: block - Hides text for up to 3s while font loads (FOIT) - Only for critical icon fonts where the system fallback is meaningless */ font-display: swap;}
/* ── System font stack — zero loading time ───────────────────────────── */body { font-family: 'Inter', /* Your preferred font */ system-ui, /* OS default: Segoe UI on Windows, SF Pro on macOS/iOS */ -apple-system, /* Safari + older macOS */ BlinkMacSystemFont, /* Chrome on macOS */ 'Segoe UI', /* Windows */ Roboto, /* Android */ 'Helvetica Neue', /* Older macOS/iOS fallback */ Arial, /* Universal fallback */ sans-serif; /* Generic fallback */}
/* ── Monospace system stack for code blocks ─────────────────────────── */code,pre,kbd { font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', ui-monospace, 'SF Mono', 'Menlo', Consolas, 'Courier New', monospace; font-size: 0.9em; /* Monospace fonts visually appear larger — scale down slightly */}<!-- ── Preload critical fonts in HTML <head> ─────────────────────────── --><!-- Only preload the font used in the first screenful (above the fold) --><!-- Preloading too many fonts HURTS performance — be selective --><link rel="preload" href="/fonts/inter-variable.woff2" as="font" type="font/woff2" crossorigin/>Variable Fonts — Responsive by Nature
Variable fonts contain an entire font family in a single file. One file holds all weights, widths, and styles as continuous axes that CSS can control with precise values.
/* ── Loading and using a variable font ──────────────────────────────── */
@font-face { font-family: 'Inter Variable'; src: url('/fonts/InterVariable.woff2') format('woff2-variations'); font-weight: 100 900; /* full range available in one file */ font-display: swap;}
/* ── Standard axis: font-weight ─────────────────────────────────────── *//* With variable fonts, font-weight can be ANY number (not just 100, 400, 700) */.hero-title { font-weight: 820; /* exactly 820 — available in variable fonts */}
.body-text { font-weight: 420; /* not bold but slightly heavier than 400 */}
/* ── font-variation-settings: access advanced axes ──────────────────── *//* 'wght' = weight axis *//* 'wdth' = width axis (condensed ↔ expanded) *//* 'ital' = italic axis *//* 'slnt' = slant axis *//* 'opsz' = optical size (designed for small vs large text) */
.condensed-heading { font-family: 'Roboto Flex', sans-serif; font-variation-settings: 'wght' 700, /* bold weight */ 'wdth' 75, /* 75% width — condensed */ 'opsz' 32; /* optical size: 32pt — optimized for display */}
.body-copy { font-family: 'Roboto Flex', sans-serif; font-variation-settings: 'wght' 400, 'wdth' 100, /* normal width */ 'opsz' 16; /* optical size: 16pt — optimized for small text */}
/* ── MAGIC: Animate font weight for interactive effects ─────────────── *//* Variable fonts can be transitioned smoothly */.nav-link { font-weight: 400; transition: font-weight 200ms ease;}
.nav-link:hover { font-weight: 700; /* No layout shift because the font was pre-loaded */}
/* ── Fluid font-weight based on viewport ────────────────────────────── *//* Get heavier as the viewport gets wider (needs the full wght range) */@supports (font-variation-settings: normal) { h1 { font-variation-settings: 'wght' clamp(600, 300 + 30vw, 900); /* Mobile: ~600 weight, Desktop: 900 weight */ }}Responsive Typography for Dark Mode
System dark mode (prefers-color-scheme: dark) requires typography adjustments beyond just color changes — contrast, weight, and size all behave differently on dark backgrounds.
/* ── Base (light mode) typography ────────────────────────────────────── */:root { --text-primary: #0f172a; /* near-black */ --text-secondary: #475569; /* medium gray */ --text-muted: #94a3b8; /* light gray */
/* Light mode: standard weight is fine */ --body-weight: 400; --heading-weight: 700;}
/* ── Dark mode typography adjustments ───────────────────────────────── */@media (prefers-color-scheme: dark) { :root { --text-primary: #f1f5f9; /* near-white — NOT pure white (too harsh) */ --text-secondary: #94a3b8; /* lighter gray */ --text-muted: #475569; /* darker for muted text */
/* IMPORTANT: In dark mode, text appears HEAVIER and LARGER than on light backgrounds due to the halation effect (light bleed on dark). Reduce font weight slightly to compensate. */ --body-weight: 350; /* slightly lighter than 400 */ --heading-weight: 600; /* slightly lighter than 700 */ }}
body { color: var(--text-primary); font-weight: var(--body-weight);}
h1,h2,h3,h4,h5,h6 { color: var(--text-primary); font-weight: var(--heading-weight);}
/* For variable fonts: smooth transition between modes */@supports (font-variation-settings: normal) { body { font-variation-settings: 'wght' var(--body-weight); transition: font-variation-settings 200ms ease; }}Accessible Typography — WCAG Compliance
Responsive typography must also be accessible. The Web Content Accessibility Guidelines specify minimum contrast ratios that are non-negotiable for professional work.
/* ── Minimum contrast ratios (WCAG 2.1) ─────────────────────────────── *//* WCAG AA (minimum compliance): - Normal text (< 18pt / 24px): 4.5:1 contrast ratio - Large text (≥ 18pt / 24px bold or 24pt): 3:1 contrast ratio
WCAG AAA (enhanced compliance): - Normal text: 7:1 contrast ratio - Large text: 4.5:1 contrast ratio
Test your contrast: https://webaim.org/resources/contrastchecker/*/
/* ── Example accessible color pairs ─────────────────────────────────── *//* These all meet WCAG AA minimum */
/* Dark on white — 18:1 ratio (far exceeds AA) */body { background: #ffffff; color: #1a1a2e;}
/* Medium gray on white — 7:1 ratio (meets AAA) */.secondary-text { color: #595959;}
/* ❌ FAILS WCAG AA on white background */.muted-text-bad { color: #aaaaaa; /* Only 2.3:1 contrast on white — DO NOT USE for readable text */}
/* ── Respecting user font size preferences ───────────────────────────── *//* NEVER override browser font size settings with fixed px on html */html { /* ✅ Don't set font-size here at all, or use a % or rem value */ font-size: 100%; /* Equivalent to keeping the browser default */}
/* ── Respecting user motion preferences ─────────────────────────────── *//* Animated text effects should be disabled if user requests it */@media (prefers-reduced-motion: reduce) { * { /* Disable font-variation-settings transitions for users who request it */ transition: none !important; animation: none !important; }}
/* ── Focus styles on text links ──────────────────────────────────────── *//* Don't remove focus outlines — they're essential for keyboard navigation */a:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; border-radius: 2px;}A Complete Responsive Type System
Putting everything together — here is a production-ready, fully responsive typography system that you can use as a foundation for any project.
/* ════════════════════════════════════════════════════════════════════════ COMPLETE RESPONSIVE TYPOGRAPHY SYSTEM Scale: 375px mobile → 1440px desktop Method: clamp() fluid scaling + CSS custom properties Accessibility: WCAG AA compliant, respects browser settings ════════════════════════════════════════════════════════════════════════ */
/* ── 1. Font loading ──────────────────────────────────────────────────── */@font-face { font-family: 'Inter Variable'; src: url('/fonts/InterVariable.woff2') format('woff2-variations'); font-weight: 100 900; font-display: swap;}
/* ── 2. Custom properties ─────────────────────────────────────────────── */:root { /* Font families */ --font-sans: 'Inter Variable', system-ui, -apple-system, sans-serif; --font-mono: 'JetBrains Mono', ui-monospace, monospace; --font-serif: 'Playfair Display', Georgia, serif; /* for editorial content */
/* Fluid type scale: clamp(min, preferred, max) */ --text-xs: clamp(0.694rem, 0.15vw + 0.65rem, 0.8rem); --text-sm: clamp(0.833rem, 0.19vw + 0.78rem, 0.958rem); --text-base: clamp(1rem, 0.23vw + 0.94rem, 1.15rem); --text-lg: clamp(1.2rem, 0.39vw + 1.09rem, 1.438rem); --text-xl: clamp(1.44rem, 0.61vw + 1.25rem, 1.797rem); --text-2xl: clamp(1.728rem, 0.91vw + 1.42rem, 2.247rem); --text-3xl: clamp(2.074rem, 1.32vw + 1.63rem, 2.809rem); --text-4xl: clamp(2.488rem, 1.87vw + 1.87rem, 3.511rem); --text-5xl: clamp(2.986rem, 2.61vw + 2.15rem, 4.389rem); --text-display: clamp(3.583rem, 3.57vw + 2.47rem, 5.486rem);
/* Line heights */ --leading-none: 1; --leading-tight: 1.1; --leading-snug: 1.3; --leading-normal: 1.5; --leading-relaxed: 1.65; --leading-loose: 2;
/* Letter spacing */ --tracking-tighter: -0.05em; --tracking-tight: -0.025em; --tracking-normal: 0em; --tracking-wide: 0.025em; --tracking-wider: 0.05em; --tracking-widest: 0.1em;
/* Measure (max line length) */ --measure-narrow: 45ch; --measure-base: 65ch; --measure-wide: 80ch;
/* Colors */ --color-text: #1e293b; --color-text-secondary: #475569; --color-text-muted: #94a3b8; --color-text-inverse: #f8fafc;}
@media (prefers-color-scheme: dark) { :root { --color-text: #f1f5f9; --color-text-secondary: #94a3b8; --color-text-muted: #64748b; --color-text-inverse: #0f172a; }}
/* ── 3. Base styles ───────────────────────────────────────────────────── */*,*::before,*::after { box-sizing: border-box;}
html { font-size: 100%; /* Respect browser default — DO NOT hardcode px here */ -webkit-text-size-adjust: 100%; /* Prevent iOS font scaling on orientation change */ text-size-adjust: 100%;}
body { font-family: var(--font-sans); font-size: var(--text-base); font-weight: 400; line-height: var(--leading-relaxed); color: var(--color-text); text-rendering: optimizeSpeed; /* faster than optimizeLegibility at body size */}
/* ── 4. Heading scale ─────────────────────────────────────────────────── */h1,h2,h3,h4,h5,h6 { font-weight: 700; line-height: var(--leading-tight); letter-spacing: var(--tracking-tight); color: var(--color-text); text-wrap: balance;}
h1 { font-size: var(--text-5xl); letter-spacing: var(--tracking-tighter); font-weight: 800;}
h2 { font-size: var(--text-4xl); letter-spacing: -0.03em;}
h3 { font-size: var(--text-3xl);}h4 { font-size: var(--text-2xl);}h5 { font-size: var(--text-xl);}h6 { font-size: var(--text-lg);}
/* ── 5. Body content ──────────────────────────────────────────────────── */p { font-size: var(--text-base); line-height: var(--leading-relaxed); max-width: var(--measure-base); text-wrap: pretty; color: var(--color-text);}
/* ── 6. Utility classes ───────────────────────────────────────────────── */.text-xs { font-size: var(--text-xs);}.text-sm { font-size: var(--text-sm);}.text-base { font-size: var(--text-base);}.text-lg { font-size: var(--text-lg);}.text-xl { font-size: var(--text-xl);}.text-2xl { font-size: var(--text-2xl);}.text-display { font-size: var(--text-display);}
.text-secondary { color: var(--color-text-secondary);}.text-muted { color: var(--color-text-muted);}
.font-light { font-weight: 300;}.font-normal { font-weight: 400;}.font-medium { font-weight: 500;}.font-semibold { font-weight: 600;}.font-bold { font-weight: 700;}.font-black { font-weight: 900;}
.leading-tight { line-height: var(--leading-tight);}.leading-normal { line-height: var(--leading-normal);}.leading-relaxed { line-height: var(--leading-relaxed);}
.tracking-tight { letter-spacing: var(--tracking-tight);}.tracking-normal { letter-spacing: var(--tracking-normal);}.tracking-wide { letter-spacing: var(--tracking-wide);}.tracking-widest { letter-spacing: var(--tracking-widest);}
.text-balance { text-wrap: balance;}.text-pretty { text-wrap: pretty;}
/* ── 7. Accessibility ─────────────────────────────────────────────────── */@media (prefers-reduced-motion: reduce) { * { transition: none !important; animation: none !important; }}
/* ── 8. Code and pre ──────────────────────────────────────────────────── */code { font-family: var(--font-mono); font-size: 0.9em; /* slightly smaller — mono fonts read large */ background: rgba(0, 0, 0, 0.06); padding: 0.1em 0.35em; border-radius: 3px;}
pre { font-family: var(--font-mono); font-size: var(--text-sm); line-height: var(--leading-relaxed); overflow-x: auto; padding: 1.5rem;}
/* ── 9. Blockquote ───────────────────────────────────────────────────── */blockquote { font-size: var(--text-xl); font-style: italic; line-height: var(--leading-snug); letter-spacing: var(--tracking-tight); border-left: 4px solid currentColor; padding-left: 1.5rem; max-width: var(--measure-narrow); text-wrap: balance;}
/* ── 10. Labels and captions ─────────────────────────────────────────── */.label { font-size: var(--text-xs); font-weight: 600; letter-spacing: var(--tracking-widest); text-transform: uppercase; color: var(--color-text-secondary);}
figcaption,.caption { font-size: var(--text-sm); line-height: var(--leading-normal); color: var(--color-text-muted); max-width: var(--measure-base); text-wrap: pretty;}Quick Reference — Typography Properties Cheatsheet
┌────────────────────────────────────────────────────────────────────────┐│ RESPONSIVE TYPOGRAPHY QUICK REFERENCE ││ ││ UNITS ││ ───────────────────────────────────────────────────────────────── ││ rem → font-size (respects browser preference, no compounding) ││ em → padding, margin (scales with component's own font-size) ││ ch → max-width for measure (character-width based) ││ px → borders, shadows, min/max constraints ││ clamp()→ everything that should scale between min and max ││ ││ SCALE ││ ───────────────────────────────────────────────────────────────── ││ Modular scale options: 1.067 (minor 2nd), 1.125 (major 2nd), ││ 1.250 (major 3rd), 1.333 (perfect 4th), 1.414 (augmented 4th) ││ ││ OPTIMAL VALUES ││ ───────────────────────────────────────────────────────────────── ││ Body font-size: 1rem–1.125rem (16px–18px) ││ Heading line-height: 1.0–1.3 ││ Body line-height: 1.5–1.7 ││ Measure (max-width): 60ch–75ch for body, 20ch–30ch for headlines ││ Letter spacing on display type: -0.02em to -0.04em ││ Letter spacing on caps/labels: 0.05em to 0.15em ││ ││ DO DON'T ││ ───────────────────────────── ────────────────────────────────── ││ Use clamp() for fluid text Use bare vw without clamp() ││ Use rem for font-size Set html font-size in px ││ Set max-width on paragraphs Let text run full-width on desktop ││ Use text-wrap: balance/pretty Leave orphaned words in headings ││ Preload only critical fonts Preload all font weights ││ Test WCAG contrast Use gray text below 4.5:1 on white ││ Adjust weight in dark mode Use pure white text on dark bg │└────────────────────────────────────────────────────────────────────────┘