Preventing font layout shifts
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)
Inter (red) vs. Arial using font-fallbacks-dataset values (blue)
Inter (red) vs. Arial using capsize values (blue)
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.)