Typography and Readability
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
| Property | Why it matters | Common 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
1remwith unitlessline-heightbetween 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=swapand preconnects.