Licenses

Writing a Font Fallback Stack That Does Not Jump

· 2 min read

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.

Questions

What should I put after my font in font-family?
A system stack — system-ui followed by the platform interface font names — and then a generic family. That gives every visitor something well-made and already installed while your font downloads, rather than whatever their browser happens to substitute.
How do I stop the page jumping when my font loads?
Match the fallback’s metrics to the real font. Declare an @font-face rule pointing at a local system font with size-adjust, ascent-override and descent-override tuned so it occupies almost the same space, then use that as your fallback. Alternatively font-display: optional removes the shift entirely, at the cost of sometimes not showing your font at all.
Do I still need serif or sans-serif at the end of the list?
Yes. It is what the browser falls back to when nothing else in the list contains the character being drawn, which happens as soon as somebody types an accent your font does not carry. It costs nothing and prevents a visible gap.
What is the difference between font-display swap and optional?
Swap shows fallback text immediately and replaces it when the font arrives, which causes a reflow. Optional gives the font a very short window and gives up for that page view if it misses, so the layout never shifts but some visitors never see your font.

Fonts in this post

  • Inter

    Sans-serif · Rasmus Andersson · Variable

  • Lora

    Serif · Cyreal · Variable