Typography is the thing nobody notices until it's wrong. A broken type scale, a layout shift from a late-loading font, a line too wide to read — these aren't aesthetic complaints. They're usability failures. Getting type right means fluid scales, proper loading, and a respect for reading measure.
A type scale is a set of font sizes derived from a ratio. Pick a base size (usually 16px) and a ratio (1.25 for "major third" is a safe default), and every step in the scale is the previous step multiplied by that ratio. This gives you a hierarchy that feels intentional rather than arbitrary.
Fluid typography uses CSS clamp() to interpolate between a minimum and maximum size as the viewport changes. No breakpoints, no jumps — the type scales smoothly. The formula: clamp(min, preferred, max) where the preferred value is a viewport-relative unit that bridges the two extremes.
Reading measure is the width of a line of text, measured in characters. The comfortable range is 45–75ch. Wider than that and the eye loses its place returning to the start of the next line. Narrower and you're breaking thoughts across too many lines. Set max-width in ch units on your prose containers.
/* Fluid type scale with clamp() — no breakpoints needed */ :root { /* Base: 16px at 320vw → 18px at 1200vw */ --text-base: clamp(1rem, 0.93rem + 0.36vw, 1.125rem); /* Scale ratio ~1.25 (major third) */ --text-sm: clamp(0.8rem, 0.77rem + 0.18vw, 0.889rem); --text-lg: clamp(1.25rem, 1.16rem + 0.45vw, 1.406rem); --text-xl: clamp(1.563rem, 1.45rem + 0.56vw, 1.758rem); --text-2xl: clamp(1.953rem, 1.81rem + 0.71vw, 2.197rem); --text-3xl: clamp(2.441rem, 2.27rem + 0.89vw, 2.747rem); --text-4xl: clamp(3.052rem, 2.83rem + 1.11vw, 3.433rem); } /* Apply to elements */ body { font-size: var(--text-base); line-height: 1.6; } h1 { font-size: var(--text-4xl); line-height: 1.05; letter-spacing: -0.02em; } h2 { font-size: var(--text-3xl); line-height: 1.15; } h3 { font-size: var(--text-2xl); line-height: 1.25; } .prose { max-width: 65ch; }
/* Font loading — preconnect + display swap + optional subsetting */ <!-- In <head> — preconnect to Google Fonts origin --> <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> /* Self-hosted @font-face with font-display: swap */ @font-face { font-family: 'Outfit'; src: url('/fonts/Outfit-Variable.woff2') format('woff2-variations'); font-weight: 100 900; font-display: swap; /* Show fallback immediately, swap when loaded */ unicode-range: U+0000-00FF; /* Latin subset — load other ranges on demand */ }
/* Variable font — one file, infinite weights and optical sizes */ @font-face { font-family: 'Inter'; src: url('/fonts/InterVariable.woff2') format('woff2-variations'); font-weight: 100 900; font-optical-sizing: auto; /* Browser adjusts for size automatically */ font-display: swap; } /* Use the weight axis directly — no need for separate files */ .heading { font-variation-settings: 'wght' 750; } .caption { font-variation-settings: 'wght' 400, 'opsz' 12; } .display { font-variation-settings: 'wght' 800, 'opsz' 48; }
If you use font-display: block, the browser hides text until the font loads — causing a visible flash. Use swap and accept the brief fallback, or preload the font with <link rel="preload"> to minimise the gap.
A line-height of 1.6 works for body text at 16px but produces too much space at 48px display sizes. Tighten line-height as font size increases: body at 1.6, headings at 1.1–1.25, display text at 1.0–1.05.
Every weight is a network request (or a larger variable font file). Audit what you actually use. Most projects need regular, medium, and bold — three weights, not six. Variable fonts solve this by containing the full weight range in one file.
Fonts designed for body text have generous letter-spacing. At display sizes (40px+), that spacing looks loose and amateurish. Add negative letter-spacing to headings: -0.02em to -0.04em depending on the font.
font-variation-settings (Edge 16 and below).