Type something to search...
What are font fallbacks and how do you tune them with size-adjust?

What are font fallbacks and how do you tune them with size-adjust?

A font fallback is the font a browser uses when your preferred font isn't available, either because it hasn't finished downloading yet or because it can't be loaded at all. You declare fallbacks in the font-family stack, such as font-family: "Inter", Arial, sans-serif, and the browser works through the list from left to right. The problem is that fallbacks rarely have the same proportions as your web font, so text reflows when the real font arrives. The size-adjust descriptor, together with ascent-override, descent-override and line-gap-override, lets you scale and reshape a local fallback font inside an @font-face rule so it occupies almost exactly the same space as your web font. The result is a much smaller layout shift and a less jarring swap.

Fallbacks matter because every visitor sees them at some point: on a slow connection, on the first visit before fonts are cached, or when a font request fails. A well-tuned fallback makes that moment almost invisible, while a poorly matched one moves buttons, pushes paragraphs down the page and hurts your Cumulative Layout Shift score. In this article you'll learn how fallback stacks work, why metrics differ between fonts, what each override descriptor does, how to calculate the values by hand or with tools, and how to test the result.

How Font Fallback Works

When the browser renders text, it walks the font-family list and uses the first family that is available and contains a glyph for the character it needs. That second condition is important: fallback happens per character, not just per element.

body {
  font-family: "Inter", "Helvetica Neue", Arial, sans-serif;
}

With this stack, the browser behaves roughly like this:

  1. Inter is used if it has loaded and contains the character.
  2. Helvetica Neue is tried next. It's installed on Apple devices but rarely elsewhere.
  3. Arial is installed on almost every Windows and macOS machine.
  4. sans-serif is a generic family that always resolves to something, so text is never left without a font.

If Inter is still downloading, the font-display value decides what happens in the meantime. With swap, the browser shows the first available fallback straight away and swaps in Inter once it arrives. With block, it hides the text for a short period first. Either way, there is often a moment when the fallback is on screen.

Per-Character Fallback

Fallback also kicks in when your font is loaded but lacks a glyph. If your web font only covers Latin characters and a user's name contains a Greek or Cyrillic letter, the browser draws that single letter from the next font in the stack. This is why a sensible stack still matters even when you self-host every font you use.

Why Fallback Fonts Cause Layout Shift

Two fonts set at the same font-size can take up very different amounts of space. Font size in CSS sets the size of the em square, an abstract box the designer draws glyphs inside. How wide the letters are, how tall the lowercase is, and how much space sits above and below each line are all decisions made by the type designer.

The metrics that cause trouble are:

  • Average character width: A wider font fits fewer words per line, so paragraphs wrap differently and become taller or shorter.
  • Ascent: The distance from the baseline to the top of the line box, as defined in the font file.
  • Descent: The distance from the baseline to the bottom of the line box.
  • Line gap: Extra space some fonts add between lines when line-height is normal.

When the web font replaces the fallback, every difference in these numbers moves text. A heading that wrapped onto two lines in Arial might fit on one line in your web font, pulling everything below it upwards. That movement is counted as layout shift.

You can reduce the line-height part of the problem by setting an explicit unitless line-height rather than relying on normal, because the line box height then comes from your value instead of each font's ascent, descent and line gap. Width differences remain, and that's where size-adjust helps.

The Four Override Descriptors

All four descriptors go inside an @font-face rule, not on regular selectors. That means you create a new, named font face that points at a local font and reshapes it.

@font-face {
  font-family: "Inter Fallback";
  src: local("Arial");
  size-adjust: 107.12%;
  ascent-override: 90.44%;
  descent-override: 22.52%;
  line-gap-override: 0%;
}

body {
  font-family: "Inter", "Inter Fallback", sans-serif;
}

Here's what each line does.

size-adjust

size-adjust multiplies the glyph outlines and metrics of the font by a percentage. A value of 107.12% makes Arial draw about 7% larger than its nominal size, which brings its average character width close to Inter's. It doesn't change the computed font-size, so em units and anything else based on font size are unaffected. Only the drawing of this particular face changes.

ascent-override and descent-override

These replace the ascent and descent values stored in the font file. They are expressed as a percentage of the font size used, which already includes the effect of size-adjust. Matching them to the web font means the line box sits in the same place, so text starts at the same vertical position whether the fallback or the web font is showing.

line-gap-override

This replaces the line gap stored in the font. Most modern web fonts have a line gap of zero, so the override is often 0%. It only affects layout when line-height is normal.

Browser Support

size-adjust works in current versions of Chrome, Edge, Firefox and Safari. The ascent, descent and line-gap overrides are supported in Chromium browsers and Firefox, but Safari support has lagged behind, so check current compatibility data before relying on them. Browsers that don't understand a descriptor simply ignore it, which makes this a safe progressive enhancement: the worst case is the same layout shift you had before.

How to Calculate the Values

The numbers come from the font files themselves. You need a few values from each font:

  • unitsPerEm: The size of the em square in font units, often 1000 or 2048.
  • ascent and descent: Usually taken from the hhea table or the OS/2 typographic metrics.
  • lineGap: The extra line spacing value.
  • Average width: Either the xAvgCharWidth value from the OS/2 table or, better, an average weighted by how often each letter appears in real text.

The formulas are:

size-adjust       = webFontAvgWidth / fallbackAvgWidth
ascent-override   = webFontAscent  / (webFontUnitsPerEm * sizeAdjust)
descent-override  = webFontDescent / (webFontUnitsPerEm * sizeAdjust)
line-gap-override = webFontLineGap / (webFontUnitsPerEm * sizeAdjust)

Widths should be normalised by each font's own unitsPerEm before you divide them, because Arial uses 2048 units per em while many web fonts use 1000 or 2048.

You divide the override values by size-adjust because the overrides are applied to the already scaled font. If you forget this, the line box ends up too tall.

Reading Metrics With fontTools

You can read the raw values with the Python fontTools library (pip install fonttools brotli, where brotli is needed for WOFF2 files):

from fontTools.ttLib import TTFont

def metrics(path):
    font = TTFont(path)
    head, hhea, os2 = font["head"], font["hhea"], font["OS/2"]
    upm = head.unitsPerEm
    return {
        "unitsPerEm": upm,
        "ascent": hhea.ascent,
        "descent": abs(hhea.descent),
        "lineGap": hhea.lineGap,
        "avgWidth": os2.xAvgCharWidth / upm,
    }

print(metrics("fonts/Inter-Regular.woff2"))

The output gives you everything the formulas need. Run it on your web font and on the fallback font file, then plug the numbers in. xAvgCharWidth is a rough measure, so the result is a starting point rather than a perfect match.

Generating Fallbacks With Tools

Doing this by hand for every font and weight is tedious, and several tools automate it with better width estimates.

Capsize

Capsize publishes metrics for thousands of fonts and can generate fallback @font-face rules for you:

npm install @capsizecss/core @capsizecss/metrics
import { createFontStack } from "@capsizecss/core";
import merriweather from "@capsizecss/metrics/merriweather";
import georgia from "@capsizecss/metrics/georgia";

const { fontFamily, fontFaces } = createFontStack([merriweather, georgia]);

console.log(fontFamily);
console.log(fontFaces);

The output contains a ready-made font-family value and the matching rule, similar to this (exact naming and values depend on the Capsize version and fonts):

Merriweather, "Merriweather Fallback: Georgia"
@font-face {
  font-family: "Merriweather Fallback: Georgia";
  src: local('Georgia');
  ascent-override: ...;
  descent-override: ...;
  size-adjust: ...;
}

Copy the generated CSS into your stylesheet, or run the script as part of your build.

Framework Support

If you use Next.js, the built-in next/font module generates an adjusted fallback face automatically for Google and local fonts. Nuxt and Vite projects can use the Fontaine plugin, which reads your @font-face rules and injects fallback faces with calculated overrides. These are worth using if they fit your stack, because they keep the numbers in sync when you change fonts.

Choosing the Right Fallback Font

Tuning metrics can only do so much. Start with a fallback that already looks similar, then adjust it.

Web font styleGood local fallbacks
Neo-grotesque sans (Inter, Roboto)Arial, Helvetica
Humanist sans (Source Sans, Open Sans)Arial, Segoe UI, Verdana
Transitional or old-style serifGeorgia, Times New Roman
MonospaceCourier New, Menlo, Consolas

A few practical points:

  • Pick fonts that exist on your audience's devices: Arial and Georgia are installed on almost every desktop. Android devices generally ship Roboto rather than Arial, so consider adding a second fallback face that targets local("Roboto").
  • Stack multiple adjusted faces: You can define one adjusted face per local font and list them in order.
  • Match weights: If your headings use a bold web font, create a bold fallback face too, because bold glyphs are wider than regular ones.
@font-face {
  font-family: "Brand Fallback";
  src: local("Arial");
  font-weight: 400;
  size-adjust: 104%;
}

@font-face {
  font-family: "Brand Fallback";
  src: local("Arial Bold");
  font-weight: 700;
  size-adjust: 101%;
}

h1, h2, h3 {
  font-family: "Brand", "Brand Fallback", sans-serif;
  font-weight: 700;
}

The percentages in this example are placeholders. Calculate yours from the actual fonts.

What About font-size-adjust?

font-size-adjust is a different, older property that you set on elements rather than in @font-face. It scales whichever font is in use so that its x-height (or another metric) matches a given ratio. It now works in all major browser engines, but it targets x-height rather than width and line box, so it's better suited to keeping mixed fonts visually consistent than to removing layout shift. For fallback tuning, size-adjust and the overrides are the more precise tools.

Testing Your Fallback

You can't judge a fallback while the web font loads in a split second on your own fast connection. Force the fallback to show and compare.

  1. Comment out the web font: Temporarily remove the web font from the stack so only the adjusted fallback is used, then take a screenshot.
  2. Restore the web font and take a second screenshot at the same viewport width.
  3. Overlay the two: Stack the screenshots in an image editor at 50% opacity. Line breaks and paragraph heights should match closely.
  4. Throttle the network: In Chrome DevTools, set the network to a slow profile and reload. Watch the swap and see how much moves.
  5. Check the Performance panel: Record a page load and look for layout shift entries around the moment fonts finish loading.

A quick way to compare in the browser is a test page that renders the same text twice:

<p style="font-family: 'Inter'">The quick brown fox jumps over the lazy dog.</p>
<p style="font-family: 'Inter Fallback'">The quick brown fox jumps over the lazy dog.</p>

If the two lines end at almost the same point, your size-adjust value is close. If the fallback line is noticeably longer or shorter, adjust by a percent or two and check again.

You can also log layout shifts to the console while testing:

new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (!entry.hadRecentInput) {
      console.log("Layout shift:", entry.value.toFixed(4), entry.sources);
    }
  }
}).observe({ type: "layout-shift", buffered: true });

Common Mistakes

  • Putting overrides on a selector: size-adjust only works inside @font-face. Writing it on body does nothing.
  • Forgetting local(): The fallback face must point at an installed font with src: local("Arial"). Using a URL defeats the purpose.
  • Using the PostScript name incorrectly: local() matches the full font name or PostScript name. "Arial Bold" and "Arial-BoldMT" are both commonly recognised, but test on your target platforms.
  • Expecting perfection: Fonts with different letter shapes will never match exactly. Aim to keep line breaks the same in typical paragraphs and headings.
  • Leaving out the generic family: Always end the stack with sans-serif, serif or monospace, in case no local font matches.

For a broader look at reducing movement during font loading, see how to prevent layout shift caused by web fonts.


FAQ: Font Fallbacks and size-adjust

A font fallback is any font later in the font-family list that the browser uses when your first choice isn't available, either because it hasn't loaded yet or because it doesn't contain a particular character.

No. It changes how large the glyphs of that specific font face are drawn, but the computed font-size stays the same, so em-based spacing and sizes elsewhere on the page are unaffected.

You can, but it's normally applied to the local fallback face. Scaling the web font itself would change your final design, while scaling the fallback only changes the brief moment before the real font loads.

An explicit line-height removes most vertical differences between fonts, but the overrides still help the text sit in the same position within each line. Use both if your browser targets support them.

Usually not completely, because letter shapes and individual widths differ. A good match keeps line breaks the same in most text, which reduces the shift to a small fraction of what it was.

Capsize can generate fallback font faces from its metrics database, the Fontaine plugin does it for Vite and Nuxt builds, and Next.js generates adjusted fallbacks automatically through next/font.


Conclusion

Font fallbacks are not an edge case. Every visitor sees them at least briefly, and a mismatched fallback is one of the most common causes of text jumping around as a page loads. By creating a dedicated @font-face that points at a local font and applying size-adjust, ascent-override, descent-override and line-gap-override, you can make that fallback take up almost the same space as your web font.

Start with a fallback that already resembles your font, calculate the values from real metrics or let Capsize, Fontaine or next/font do it for you, and test by overlaying screenshots and watching the swap on a throttled connection. A few minutes of tuning gives you a calmer page load and a better layout stability score with no downside for browsers that ignore the descriptors.

Share :

Related Posts

How to make typography accessible?

How to make typography accessible?

You make typography accessible by choosing clear typefaces, setting text in relative units so it scales with user preferences, giving it enough colou

Dive Deeper
What are the parts of a letterform?

What are the parts of a letterform?

A letterform is the shape of a single letter, and typographers break it down into named parts. The main ones are the stem (the main vertical

Dive Deeper
What are ascenders, descenders and the baseline?

What are ascenders, descenders and the baseline?

The baseline is the invisible line that letters sit on. Ascenders are the parts of lowercase letters that rise above the x-height, such as th

Dive Deeper