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. This is bad for user experience, and affects your Cumulative Layout Shift (CLS) score in Google Lighthouse.

Next.js users can use @next/font to prevent this from happening. It accomplishes this by creating a fallback @font-face using a local font such as Arial and using 4 CSS properties: size-adjust, ascent-override, descent-override, and line-gap-override. These properties are set to make the fallback font as identical as possible to the desired font.

For example, here is what @next/font generates for the Inter font:

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

@font-face {
  font-family: "__Inter_Fallback_ccafe3";
  src: local("Arial");
  ascent-override: 90.2%;
  descent-override: 22.48%;
  line-gap-override: 0%;
  size-adjust: 107.4%;
}

By setting the font-family property to __Inter_ccafe3, __Inter_Fallback_ccafe3, the browser will use the fallback font until the Inter font has loaded.

Note that this technique does not work in Firefox. For some reason, Firefox skips the fallback font and uses another font.

Calculating the values

This Chrome blog post suggests using either Fontaine or copying the values from font-fallbacks-dataset. Unfortunately, neither of them use size-adjust, so the fallback font will not be as identical as it could be.

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

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

import { createFontStack } from "@capsizecss/core";
import inter from "@capsizecss/metrics/inter.js";
import arial from "@capsizecss/metrics/arial.js";

const { fontFaces } = createFontStack([
  inter, // Desired font
  arial, // Fallback font (Arial is the most "web safe" font)
]);

console.log(fontFaces);

Output:

@font-face {
  font-family: "Inter Fallback";
  src: local("Arial");
  ascent-override: 90.199%;
  descent-override: 22.4836%;
  size-adjust: 107.4014%;
}

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 />

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

Go back