Writing a Font Fallback Stack That Does Not Jump

· 5 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.

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-override and descent-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

  1. Your web font first, named exactly as the @font-face declares it.
  2. An adjusted local fallback, if you have measured one — this is the rule above.
  3. The system stack for the same category, so a machine with no web font gets something appropriate rather than something arbitrary.
  4. 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.

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.
What does size-adjust do in CSS?
It scales a font up or down inside its own @font-face rule, as a percentage. Used on a local fallback font, it makes that fallback occupy the same space as your web font, so the page does not reflow when the real font finishes loading.

Fonts in this post

  • Inter

    Sans-serif · Rasmus Andersson · Variable

  • Lora

    Serif · Cyreal · Variable

Find the font for your next project.

All 337 families here are free for commercial use, and every download carries its license inside the zip.