Typography and Readability

Typography is the one CSS discipline where every user sees every decision. Line length, line height, and font size determine whether a page is pleasant or exhausting to read — and unlike layout bugs, bad typography rarely announces itself. It just loses the reader.

What readability means in CSS

The measure, the leading, the size

Three numbers decide whether prose is readable. The measure is the line length — 45 to 75 characters; beyond that, eyes lose the line when jumping back. The leading is line height — 1.5 to 1.7 for body text in Latin scripts. The size is body text — at least 1rem, which respects the user's browser setting. Every other typographic choice is secondary to these three.

/* The three decisions in one place */
.prose {
  font-size: 1rem;        /* the user's chosen size - do not shrink it */
  line-height: 1.6;       /* unitless: scales with the font, never drifts */
  max-width: 68ch;        /* the measure, in characters, not pixels */
}

Why line-height must be unitless

A unitless line-height: 1.6 computes per element from its own font size, so headings and small print keep proportion. A fixed line-height: 24px or even an inherited 1.6em bakes one font size into every descendant — small text gets airy, large text gets cramped. Unitless is the only choice that survives inheritance.

Core properties

PropertyWhy it mattersCommon starter value
font-family Controls tone and readability; the stack is a fallback chain system-ui, sans-serif
font-size Sets base reading size; everything else is relative 1rem for body
line-height Improves line scanning; unitless value scales with font 1.5 to 1.7 body, 1.1–1.25 headings
letter-spacing Fine-tunes density; helps small caps and display type Default body; slight positive on all-caps labels
font-weight Creates hierarchy; use the weights the font file actually ships 400 body, 600+ headings
text-wrap Balances short lines on headings text-wrap: balance on headings

Font stacks

System stacks first

Every modern OS ships quality text faces. A system stack costs zero requests, renders instantly (no flash of invisible or fallback text), and covers every script. Reach for a web font only when branding requires it — and then load it deliberately (below).

/* Zero-download stacks: ordered from most to least preferred */
body { font-family: system-ui, sans-serif; }          /* UI text */
.prose { font-family: Charter, Cambria, Georgia, serif; }  /* long-form */
code, pre { font-family: ui-monospace, Consolas, monospace; } /* code */

A web font is a caching decision

Each web font face is a file the browser must download before it can render text. Keep the count honest: one or two families, only the weights you actually use (a variable font file can cover all weights in one download), and always font-display: swap so text renders in the fallback first and upgrades when the real font arrives.

<!-- preconnect warms the font host's connection before the CSS is parsed -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;600&display=swap"
      rel="stylesheet">

A baseline stylesheet

The ten lines that run most sites

/* Typography baseline: set the defaults once, inherit everywhere */
body {
  font-family: system-ui, sans-serif;  /* no download, no layout shift */
  line-height: 1.6;                    /* unitless - scales per element */
  color: #e5e7eb;
}

h1, h2, h3 {
  line-height: 1.2;          /* headings are short: tighter leading */
  text-wrap: balance;        /* avoids one-word last lines on narrow screens */
  margin-bottom: 0.5em;      /* em: proportional to the heading's own size */
}

p {
  margin-bottom: 1rem;
  max-width: 70ch;           /* the measure - applies to prose containers */
}

Note what is absent: no html { font-size: 16px } (it would erase the user's preference), no fixed pixel sizes, and no !important. The baseline wins by being the default, not by force.

Fluid type

Scale with the screen, without media queries

A display heading can safely grow on wide screens if it is clamped. The preferred value mixes rem (so it respects the user's base size) with vw (so it tracks the screen); the floor and ceiling keep it sane everywhere:

h1 { font-size: clamp(1.75rem, 1.2rem + 2.5vw, 3.25rem); }
h2 { font-size: clamp(1.35rem, 1.1rem + 1.5vw, 2rem); }
small, .caption { font-size: 0.875rem; }  /* floor at 14px-equivalent: never smaller */

Hierarchy without more fonts

Use what the type scale already has

A hierarchy needs contrast, not more typefaces: vary size, weight, case, and color — in that order. Two families (one for headings, one for text, or text plus monospace) is enough for nearly any site; a third is rarely justified by the download.

/* Hierarchy from weight, size and case - one family */
.eyebrow {
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.08em;   /* all-caps labels need room to breathe */
  text-transform: uppercase;
  color: #94a3b8;
}

Practice: a typography pass

The task

Open demo/box-and-text/11_typography_scale.html and demo/box-and-text/12_fluid_type.html from the Lab Examples page, then apply the same treatment to a page you have already built.

The checklist

  • Body text is 1rem with unitless line-height between 1.5 and 1.7.
  • Prose containers are capped near 70ch — check a full-width paragraph on a wide window.
  • Heading levels are distinguishable by more than color alone (size or weight at minimum).
  • No text below 0.875rem, and contrast between text and background passes WCAG AA.
  • Web fonts, if any, load with display=swap and preconnects.