Workshop · back to the section

A metric-matched fallback font

When the web font arrives, the text reflows. That isn't inevitable — you just have to work out how much room the fallback should occupy until then.

The symptom is familiar: the page loads, shows in a system font for a moment, then the web font lands and everything moves. Lines break elsewhere, paragraph heights change, images slide down. Anyone who had started reading loses their place.

The received advice is to set font-display. But none of its values solves the problem; they merely decide which downside you take: either the fallback shows and the text jumps, or nothing shows until the font arrives, or the final font never gets to appear.

The actual cause

The text doesn't jump because there are two fonts, but because they occupy different space. They differ in two ways:

In width. The same word at the same size has a different length in the two fonts. Line breaks land elsewhere, and a paragraph can end up a line longer or shorter.

In height. Line height comes not from the font size but from the ratio of the font's ascender and descender — and that differs between typefaces. Two fonts at identical size produce different line spacing.

Without matching and with it Without matching the fallback occupies different space, so content moves when the font arrives; with matching both states are the same size and nothing shifts. without matching fallback · different width and height with matching fallback · matched metrics without matching the final font · everything moves with matching the final font · same space the swap is the same — the difference is whether it shows
without matchingfallback · different width and height
with matchingfallback · matched metrics
without matchingthe final font · everything moves
with matchingthe final font · same space

the swap is the same — the difference is whether it shows

The four properties

The fix is not to use the fallback as it comes, but to declare it with a rule of your own and adjust its metrics.

size-adjust applies a percentage multiplier to the fallback's drawn size without changing the declared font size — matching its character widths to the final font. Get that right and line breaks fall in the same places in both states.

ascent-override and descent-override set the ascender and descender, and therefore the line height. Without those two, line lengths would already be right while paragraph heights would still jump. line-gap-override settles the base spacing between lines where that matters.

The calculation

The values don't come from trial and error but from the two fonts' metrics. The width ratio gives size-adjust; the ascender and descender sizes — relative to the font's em square, then corrected by the multiplier — give the two overrides.

The essential part is that the result must be measured, not eyeballed. With correct settings a given paragraph's height is identical to the pixel before and after the swap. If it differs, one of the values is wrong — and the eye won't tell you about a few pixels.

One thing worth knowing: the fallback font must actually exist on the visitor's machine, or the matching applies to something that never runs. That's why the rule uses the local() form with several common names, so that the most widespread systems are certain to have a hit.

What you get for it

The visible result is that the font swap becomes imperceptible: the text stays put and only the letterforms change. Anyone not looking for it won't notice — and that is exactly the goal.

There's a wider lesson here: font loading is also a transition whose intermediate state is visible — the same question as with scroll-driven animation, where a faulty intermediate state produces an empty page.

The measurable result is that layout shift disappears. It's one of the metrics search engines watch, but that isn't the main reason: someone trying to read a jumping page leaves at the second jump.

Questions on this topic

Why does text jump when a font loads?

Because until the web font arrives the browser draws with a system font that has different character widths and a different line height. When the web font lands, the text reflows: line lengths change, paragraph heights jump, and everything below moves.

What does size-adjust do?

It applies a percentage multiplier to the drawn size of the fallback, without changing the declared font size. That lets the fallback's character widths be matched to the final font, so line breaks fall in the same places in both states.

Why are ascent-override and descent-override needed too?

Because size-adjust only settles the width. Line height comes from the ratio of the font's ascender and descender, and that differs between typefaces. Without those two the line lengths would already match while paragraph heights would still jump.

How do I know what values to use?

By calculating from both fonts' metrics: the width ratio gives size-adjust, and the ascender and descender sizes give the two overrides. These can be read out of the font files, and the result should be verified by measurement — with correct settings a paragraph's height is identical before and after the swap.

← Back to the Workshop