A font-family declaration is a list, and the browser walks it. Most people write their chosen face, then sans-serif, and never think about it again. That second entry is what every visitor reads while the real font downloads, and how different it is decides whether the page jumps when the swap happens.
What the browser actually does
It works left to right, per character. If the first family is not available — still downloading, failed, or missing that particular glyph — it moves to the next. So the list is not only a fallback for failure; it is also what fills in for characters your font does not contain.
The system stack, and why it beats a single name
Writing Arial as your fallback means Windows users get Arial and everyone else gets whatever their browser substitutes. The system stack names the native interface font on each platform instead, so every visitor gets something well-made and locally installed: system-ui first, then the platform-specific names, then a generic.
It renders instantly on every device because nothing is downloaded, and it looks native rather than borrowed. For a great many projects it is a legitimate final answer rather than a fallback.
Matching the metrics, which is the part that stops the jump
When the real font arrives, every line re-measures. If the fallback was narrower, text reflows and the page shifts — and Cumulative Layout Shift is a ranking signal, so this is not only cosmetic.
The fix is an @font-face rule that points at a local system font and adjusts it: size-adjust scales it, ascent-override and descent-override set the line box. Tune those until the fallback occupies almost exactly the space the real font will, then name that adjusted family as your fallback. The swap becomes nearly invisible.
The generic at the end is not decoration
Always end with sans-serif, serif or monospace. It is what the browser uses when nothing else in the list has a glyph — which happens more than people expect the moment a user types a name with an accent your font does not carry. Every CSS snippet generated on this site ends with the right generic for that font’s category.
And the setting that decides what happens meanwhile
font-display: swap shows the fallback immediately and swaps when the font arrives. optional gives the font a very short window and abandons it for that page view if it misses, so nothing ever shifts. With a well-matched fallback, swap is fine. Without one, optional is often the better trade. The page speed post has the rest.
The four descriptors that make the swap invisible
Matching the metrics by eye has a proper mechanism behind it. A second @font-face rule can point at a local system font and correct its measurements to match your web font, so the fallback occupies the same space to the pixel.
size-adjust: scales the fallback up or down as a percentage, which is the one that does most of the work.ascent-overrideanddescent-override: set where the line box sits, so the text does not shift vertically when the real font arrives.line-gap-override: removes a gap the system font has and yours does not, which is what makes paragraphs change height.
Finding the numbers is trial and error with a screenshot: load the page with the web font blocked, overlay it on the same page with the font loaded, and adjust the percentage until the lines land in the same place. Ten minutes once, per font, and the layout stops moving for every visitor afterwards.
Building the stack, in order
- Your web font first, named exactly as the @font-face declares it.
- An adjusted local fallback, if you have measured one — this is the rule above.
- The system stack for the same category, so a machine with no web font gets something appropriate rather than something arbitrary.
- A generic — sans-serif, serif, monospace — at the end, always.
Keep the list short. Ten names is not more robust than four; it is four decisions and six guesses about machines you cannot see, and each guess is another face the page might render in.
Testing it without a slow connection
You cannot judge a fallback on a fast machine where the font is already cached. Three ways to see what a first-time visitor sees, in increasing order of realism: throttle the network in the browser’s developer tools; block the font file’s address outright so the fallback is permanent; or open the page in a private window with the cache disabled.
What you are looking for is not whether the fallback is pretty. It is whether the paragraph is the same number of lines before and after the swap. If it is, the page will not jump — and that number is a Core Web Vital, which is the other reason to care. The file-size side of the same question is in what your fonts cost your page speed.
Emoji, symbols and the font you did not choose
Every stack has an invisible last member. When a character is in none of your fonts, the browser reaches for a system font — and for emoji, arrows, maths signs and box-drawing characters that is exactly what happens, because text fonts rarely contain them.
Usually that is fine and invisible. It stops being invisible when the substituted character is a different weight or colour from the text around it: a black arrow beside light grey text, or an emoji that arrives in colour where you wanted a glyph. If a symbol matters to the design, either choose a font that contains it or set it deliberately in a font you picked, rather than leaving it to whichever one the operating system reaches for first.
