Preventing font layout shifts

Published on (last modified on )

Custom fonts may not be immediately available when a page loads, causing the browser to initially use a fallback font that may have different spacing. This leads to a shift in the layout of the page, which degrades user experience, and negatively impacts your Cumulative Layout Shift (CLS) score in Google Lighthouse.

A great strategy to minimize font layout shifting is by creating a fallback @font-face using a common local font such as Arial, combined with 4 CSS properties: ascent-override, descent-override, line-gap-override, and size-adjust. These properties are set to make the font-face as identical as possible to the desired font.

A library that uses this technique is @next/font. For example, here is what @next/font generates for the Inter font:

@font-face {
  font-family: "Inter";
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url(...) format("woff2");
  unicode-range: ...;
}

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

.__className_d65c78 {
  font-family: "Inter", "Inter Fallback";
  font-style: normal;
}

Update: My pull request to add size-adjust support to Fontaine has been merged. You can use it to accomplish the same if your project uses Vite.

Calculating the values

This Chrome blog post points to a repository called font-fallbacks-dataset. However, since the dataset lacks size-adjust values, the fallback fonts may not match as closely as they otherwise could.

I found that best way to calculate the values for any font is by using the createFontStack function provided by capsize:

import { createFontStack } from "@capsizecss/core";
import { fromFile } from "@capsizecss/unpack";
import inter from "@capsizecss/metrics/inter";
import arial from "@capsizecss/metrics/arial";

{
  const metrics = await fromFile("./GeistVariableVF.woff2");
  const { fontFaces } = createFontStack([metrics, arial]);
  console.log(fontFaces);
}

{
  const { fontFaces } = createFontStack([inter, arial]);
  console.log(fontFaces);
}

Output:

@font-face {
  font-family: "Geist Variable Fallback";
  src: local("Arial"), local("ArialMT");
  ascent-override: 91.9589%;
  descent-override: 21.9902%;
  line-gap-override: 9.9955%;
  size-adjust: 100.0447%;
}

@font-face {
  font-family: "Inter Fallback";
  src: local("Arial"), local("ArialMT");
  ascent-override: 90.4365%;
  descent-override: 22.518%;
  line-gap-override: 0%;
  size-adjust: 107.1194%;
}

You can easily run this script in your browser here.

Comparisons

Inter (red) vs. regular Arial (blue)

Comparison image

Inter (red) vs. Arial using font-fallbacks-dataset values (blue)

Comparison image

Inter (red) vs. Arial using capsize values (blue)

Comparison image

Conclusion

Using a custom fallback font is a great technique to prevent layout shifts. Of course, other improvements such as caching and preloading fonts still apply.

To preload a font, put this in the head element:

<link rel="preload" href="..." as="font" type="font/woff2" crossorigin />

(crossorigin is required, even if the request is not cross-origin.)

Go back