Lesson 1 of 5
Typography on the Web
"Typography exists to honour content." — Robert Bringhurst, The Elements of Typographic Style (1992)
Almost everything on a web page is text. Not most things — almost everything. Strip out the images and the chrome and what remains is words, set at a size, on lines of a length, with space between them, in a face someone chose. Which means typography is not a decorative layer applied to a finished interface. It is the interface, for the overwhelming majority of what your users actually read.
Engineers tend to treat this as a designer's department, and there is a reasonable version of that instinct: choosing a typeface is a judgement call and not one most of us are trained for. But the parts that go wrong in production are not judgement calls. They are mechanical, they have right answers, and they are almost all invisible until someone with a slow connection, a large font size, or a long name arrives.
This chapter is about those parts. Why line-height: 24px and line-height: 1.5 behave completely differently in a nested component. Why a web font that arrives 400ms late reflows the entire page and what size-adjust does about it. Why the same paragraph is comfortable at 600px and unreadable at 1200px. And why the ch unit exists.
The thing to carry: line-height must be unitless. A unit inherits its computed value, so a 24px line-height set on a container becomes 24px on a 32px heading inside it — a ratio of 0.75, and the lines overlap. A unitless value inherits the ratio, which is what you meant. One character of difference, and it is the single most common typographic bug in component-based code.
You set line-height: 24px on a container — a sensible value for 16px body text. A heading inside it, at 32px, renders with its lines overlapping. You did not set a line-height on the heading. What did it inherit, and what would 1.5 have inherited instead?
Line-height must be unitless
Run to execute the tests…
The fourth test closes the obvious escape route. em is not a fix — it resolves to a length at the parent and then inherits that length, exactly like px. Only a bare number inherits as a ratio.
Telling a class "leave three centimetres between your lines" versus "leave one and a half times your letter height". The first is fine until someone writes in large letters, at which point their lines collide and they have followed your instruction exactly. The second scales to whoever is writing. Unitless line-height is the second instruction; a unit is the first, and CSS gives you no warning that you have handed out the wrong one.
Line length is the measurement that matters most
Run to execute the tests…
Web fonts, and the reflow they cause
Run to execute the tests…
The third test is the one worth acting on. size-adjust and the metric overrides on your fallback remove the reflow entirely, which used to be a genuine trade-off and is now just work you have not done.
See the failures
Panel 1 reports its own computed values, so you can see the 0.75 ratio rather than take my word for it. In panel 3, note the measured ch width for the actual font — that is why the unit exists.
Four defaults I set before writing any other CSS. line-height unitless, always, with about 1.5 for body and tighter ratios for headings — larger text needs less leading, and a single global value is always wrong for something. max-width in ch on anything that is prose, usually around 66, because it is the one typographic decision with the largest effect on whether text is actually read. font-display: swap plus fallback metric overrides, because the reflow that used to make swap a compromise is now solvable and most teams have not noticed. And a system font stack as the fallback rather than a generic sans-serif, since the generic keyword resolves to something with unpredictable metrics. The habit that catches the most, though, is checking the page with web fonts blocked in devtools — not to see if it looks acceptable, but to see how much it moves. If the layout jumps significantly, real users on slow connections are getting that jump, and it is a measurable part of the layout-shift score Part VI is about.
@font-face was in the CSS2 specification in 1998, and it took roughly a decade to become usable — not for technical reasons but for licensing ones. Type foundries had a business model built on selling files that were installed locally, and serving a font over the web made the file trivially downloadable by anyone. The impasse broke with WOFF in 2009, a compression wrapper that added metadata foundries could point to as a licensing boundary, plus web-specific licences and services like Typekit that handled enforcement. Within about three years the web went from a handful of "web-safe" faces to essentially the entire commercial type catalogue. What is worth noticing is that the enabling change was not technical capability — browsers could have done this in 1998 — but a commercial arrangement everyone could live with. The web has a lot of that history, and it is a useful corrective to the assumption that platform features are gated by engineering.
A publisher's article pages had a layout-shift score that failed Core Web Vitals, and the team spent two sprints on images — dimensions, aspect-ratio, lazy loading — moving the score barely at all. The actual cause was the headline font. Their brand face was noticeably narrower than the fallback, so when it loaded, headlines that had wrapped to three lines rewrapped to two, and everything below jumped up by a line height. On a fast connection the swap happened before anyone looked; on a 3G connection it happened three seconds in, mid-read. The fix was two declarations: a @font-face block for the fallback with size-adjust and metric overrides tuned so the fallback occupied the same space, and font-display: swap. Their score passed within a day. The part I found instructive is that the images work was not wrong — it was just optimising the thing that was easy to see. A font swap is invisible on the machine you develop on, which is precisely the profile of a defect that survives two sprints of attention aimed elsewhere.
Open a long article on any site you like and measure its line length — count the characters in a full line, or set max-width in devtools until you can. Then read two paragraphs at that width, and two more with the measure narrowed to about 66 characters. The difference is not subtle once you are looking for it, and what you will notice is not that the narrow version is prettier: it is that you stop losing your place. Now ask why so few sites do this, given that it costs one declaration and has been settled typographic practice for five hundred years. My honest read is that a wide measure looks efficient — it fills the space, it feels like using the screen you were given — and the cost is entirely on the reader, invisible in any review and unmeasured by anything. That is a shape you should recognise by now: the decision is made where the cost is not felt.
Common Mistakes
line-heightwith a unit. It inherits a computed length, so a 24px line-height on a 32px heading is a ratio of 0.75 and the lines overlap.Thinking
emfixes it.1.5emresolves to a length at the parent and inherits likepx. Only a bare number is a ratio.One global line-height. Larger text needs a tighter ratio; the relationship is inverse.
Leaving the browser default. About 1.2 — fine for headings, tight for body text, and a common reason a page feels dense.
Measures over 75 characters. Readers lose the return sweep and reread lines without noticing. 1200px is about 141 characters.
max-widthinpxfor prose. It is correct only for the font it was measured against;chtracks the typeface and the user's size.A wide measure with tight leading. The worst combination, and endemic in dashboards.
font-display: block(or the default). Up to three seconds of invisible text.swapwith no metric overrides. Readable immediately and reflows the page — nearly 8% wider lines in the worked case.size-adjustremoves it.Shipping six static font files where one variable font covers the same range, often smaller than two of them.
No subsetting. A full Unicode face against a Latin subset was 84% larger.
Testing with the font cached. The swap is invisible on your machine, which is why it survives sprints of attention aimed elsewhere.
Interview Corner
line-height be unitless?ch unit, font-display semantics, metric overrides.Search your stylesheets for line-height with a unit and convert every one to a ratio, then look for headings whose leading changed — those were bugs nobody had reported. Set max-width: 66ch on your longest piece of prose and read it before and after. Next, block web fonts in devtools, reload, and record how far the page moves; then add a fallback @font-face with size-adjust tuned until the shift is gone, and measure again. Check what your font files actually cost — count the weights and styles you ship, compare against a variable font, and check whether you are subsetting. Finally, throttle to slow 3G with the cache disabled and watch your own headlines rewrap; that is the experience the metric is measuring and the one you never see.
Run to execute the tests…
Summary
- Almost everything on a page is text, so typography is the interface for most of what users read.
line-heightmust be unitless. A unit inherits a computed length: 24px on a 32px heading is a ratio of 0.75, and lines overlap.emdoes not fix it — it resolves to a length at the parent and inherits likepx.- Larger text needs a tighter ratio, so one global line-height is always wrong somewhere. The UA default of ~1.2 is tight for body text.
- 45–75 characters is the comfortable band; 66 is the classic target, around 561px at typical body sizes.
- 1200px is about 141 characters — nearly twice the upper bound, and readers lose the return sweep.
chtracks the typeface and the user's size;pxis correct only for the font it was measured against.chis approximate — "0" is wider than average, so 66ch is about 72 real characters.font-displayis a choice between failure modes: invisible text (block) or reflowing text (swap).- Metric overrides on the fallback remove the reflow.
size-adjustplus ascent/descent tuning makesswapfree. optionalguarantees zero shift at the cost of the first visit being off-brand.- Six static files versus one variable font covering the same range, often smaller than two of them.
- Subsetting was 84% in the worked case; most fonts ship scripts you do not use.
- Test with fonts blocked and measure the movement, not the appearance — the swap is invisible on your machine.
In the next chapter, Colour & Contrast, we look at why the colour model most of us use daily lies about lightness — and what that costs when you build a palette on it.
Line length is the clearest case I know of a settled question that the web keeps re-losing. It was worked out by printers over centuries, it is not controversial, it costs one declaration, and the majority of text on the web still exceeds it — often by a factor of two. When I ask why, the answer is usually that a narrow column looks like wasted space on a wide screen, which is a real perception and completely beside the point: the screen is not the thing being used, the reader is. What makes this stick with me is that it is not an engineering failure or a knowledge failure. Everyone involved could recite the rule. It loses to a visual instinct about filling available space, held by people who are looking at the layout rather than reading it. That is worth watching for generally, because a lot of interface decisions have that structure — the person deciding is in a different relationship to the artefact than the person using it, and the difference is invisible from the deciding chair.
Q&A · 0
Enrol to ask questions and join the discussion.
No questions yet — be the first to ask.