Skip to Content
Stop Layout Shift From Web Fonts
Advanced
0 likes
Tutorials

Stop Layout Shift From Web Fonts

Compute size-adjust and the vertical overrides from the font files, build the fallback face, and measure the layout shift before and after.

Font swaps shift the page when the fallback is a different size from the web font. You can measure the difference and build a fallback that takes the same room, so the swap is hard to see.

Step 1: get the numbers from the font files

You need two files: the web font you load, and a local font that matches the fallback you want (Arial, or Liberation Sans on Linux, which has the same metrics). This script reads both with fonttools 4.67.0 and compares the width of a sample sentence.

from fontTools.ttLib import TTFont

SAMPLE = "The quick brown fox jumps over the lazy dog. Orders ship in two days and returns are free for thirty days. "

def measure(path):
    font = TTFont(path)
    upm = font["head"].unitsPerEm
    cmap, hmtx, hhea = font.getBestCmap(), font["hmtx"], font["hhea"]
    width = sum(hmtx[cmap[ord(c)]][0] for c in SAMPLE if ord(c) in cmap) / upm
    return width, hhea.ascent / upm, -hhea.descent / upm, hhea.lineGap / upm

web = measure("DejaVuSans.ttf")
fallback = measure("LiberationSans-Regular.ttf")

size_adjust = web[0] / fallback[0]
print(f"size-adjust: {size_adjust * 100:.1f}%")
print(f"ascent-override: {web[1] / size_adjust * 100:.1f}%")
print(f"descent-override: {web[2] / size_adjust * 100:.1f}%")
print(f"line-gap-override: {web[3] / size_adjust * 100:.1f}%")

Output:

size-adjust: 114.4%
ascent-override: 81.1%
descent-override: 20.6%
line-gap-override: 0.0%

Use the web font's own file here. DejaVuSans.ttf stands in for yours, since it is the one I tested with.

Step 2: write the fallback face

@font-face {
  font-family: "Demo Sans Fallback";
  src: local("Arial");
  size-adjust: 114.4%;
  ascent-override: 81.1%;
  descent-override: 20.6%;
  line-gap-override: 0%;
}

body { font-family: "Demo Sans", "Demo Sans Fallback", Arial, sans-serif; }

Step 3: measure the shift

let cls = 0;
new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (!entry.hadRecentInput) cls += entry.value;
  }
  console.log("CLS so far:", cls);
}).observe({ type: "layout-shift", buffered: true });

Paste this in the console before the font arrives, or add it with a script on a test build. I ran it on two pages in Chrome 154, with the web font served 0.8 seconds late:

plain fallback (Arial)      CLS 0.0165
adjusted fallback           CLS 0

On the test machine, Arial maps to Liberation Sans, so the demo added local("Liberation Sans") as a second source. On Windows and macOS, local("Arial") is the one that applies.

⚠ Warning: Run the script against each weight and style you load. Bold has different widths from regular, so it needs its own fallback face with its own numbers.

Check that it worked

Reload with the network throttled and open the Layout Shifts track in the Performance panel. There should be no shift at the moment the font arrives. If there is one, check that the fallback face loaded: a name that does not match any installed font gives a silent fallback to the default.

Common mistakes

  • Setting size-adjust but not the vertical overrides, so lines still move.
  • Naming the fallback face and forgetting to put it in font-family.
  • Measuring only on fast Wi-Fi, where the swap happens before anyone could see it.

Related course: HTML and CSS Advanced.

About

Compute size-adjust and the vertical overrides from the font files, build the fallback face, and measure the layout shift before and after.

Responsible
John G
Last Update
08/27/2026
Reading Time
3 min
Was this helpful?
​ 0 ​ 0

Comments (0)

Sign in to comment

No comments yet.