Fix layout shift caused by web fonts
Text that jumps when a web font loads. How to confirm fonts are the cause, work out fallback font overrides, and choose font-display so the swap moves nothing.
In this guide
What happens at the swap
Until a web font arrives, the browser draws text in a fallback font, or in invisible text that takes up the space of the fallback. When the web font is ready, the browser swaps it in. If the two fonts have different proportions, the page changes in two ways:
- Wider or narrower letters. Lines break in different places, so a paragraph may gain or lose a line.
- A different line height. Each line is taller or shorter, because fonts carry their own ascent, descent and line gap.
Either change pushes everything below the text up or down. That is a layout shift, and it counts towards Cumulative Layout Shift.
Hiding the text does not remove the problem. MDN describes the block period as rendering an invisible fallback face. The invisible text still takes up room, so the swap moves the page when the real font arrives. The web fonts guide covers loading fonts well; this guide is about the shift.
Step 1: confirm that fonts are the cause
- Check the Lighthouse report. The insight called Layout shift culprits lists the biggest shifts and, for each, the reasons it can recognize. One of them is labelled Web font. Lighthouse blames a font when a font request happened shortly before a paint in which the layout shifted. Other labels are an injected iframe, an animation and an unsized image.
- Watch the shift happen. In Chrome DevTools open the Rendering tab, tick Layout Shift Regions and reload. Areas that move are briefly highlighted in purple. If the highlight covers paragraphs and headings when the text changes face, fonts are the cause.
- Remove the font and compare. In DevTools open the Command Menu (Ctrl+Shift+P or Cmd+Shift+P), type
block, choose Show Request Blocking and add the address of your font file as a pattern. Reload. The page now stays in the fallback font. If the shift disappears, the swap was the cause, and the fallback you see is the one you need to match. - Test on a slow connection. The swap only happens when the font is not yet available, so on a fast connection with a warm cache you may not see it. Tick Disable cache and throttle the network.
Fonts tend to hurt first-time visitors most. Returning visitors usually have the font cached, so the swap is rare for them.
Step 2: shrink the window for the swap
The shorter the time between the first paint and the font arriving, the less visitors notice, and the more often the font is ready before the first paint.
- Preload the one or two fonts the first screen uses. The preload must include
crossorigin, as explained in the resource hints guide. - Serve fewer and smaller files: WOFF2, a subset of characters, only the weights you use.
- Serve the fonts from your own address, so there is no extra connection to open.
Step 3: choose a font-display value
font-display decides what happens while the font loads.
| Value | Effect on layout shift |
|---|---|
swap | Text shows at once in the fallback and swaps when the font arrives. The swap can shift the page unless the fallback matches |
optional | Extremely small block period and no swap period. If the font is not ready almost immediately, the page stays in the fallback for the whole visit. Nothing swaps, so nothing shifts |
fallback | A short swap period. A late font is not swapped in, which limits shifts, but the text may still move if the font arrives inside the short period |
block | Invisible text first, then a swap. The page still shifts when the font arrives |
auto | The browser decides |
optional is the safest value for layout stability, with a price: a first-time visitor on a slow connection may not see your font on that first page, and it can be used from the next page on, once it is cached. Pair it with a preload so the font is ready in time for most visits. Lighthouse's Font display insight points to swap or optional and adds that swap can be improved with font metric overrides, which is the next step.
Step 4: make the fallback take the same space
If you want the swap, give the fallback the same dimensions as the web font. CSS has four descriptors for this on @font-face:
size-adjustscales the glyphs and their widths by a percentage.ascent-override,descent-overrideandline-gap-overrideset the vertical metrics as a percentage of the font size.
You use them on a new @font-face whose source is a font already on the visitor's device:
@font-face {
font-family: "Brand Fallback";
src: local("Arial");
size-adjust: 104%;
ascent-override: 91.35%;
descent-override: 24.04%;
line-gap-override: 0%;
}
body { font-family: "Brand", "Brand Fallback", sans-serif; }The text is first drawn in the adjusted Arial, which takes the same room as "Brand". When "Brand" arrives, the lines break in the same places and have the same height.
Where the numbers come from
The Chrome team gives these equations. The metrics belong to the web font, and the average character width is needed for both fonts:
size-adjust = avgCharWidth(web font) / avgCharWidth(fallback)
ascent-override = ascent / (unitsPerEm × size-adjust)
descent-override = descent / (unitsPerEm × size-adjust)
line-gap-override = lineGap / (unitsPerEm × size-adjust)For example, say the web font has 1,000 units per em, an ascent of 950, a descent of 250 and no line gap, and its average character width is 520. Say the local Arial has an average width of 500. Then size-adjust is 520 ÷ 500 = 104%. The ascent override is 950 ÷ (1,000 × 1.04) = 91.35%, and the descent override is 250 ÷ 1,040 = 24.04%. These numbers are invented to show the arithmetic, which is why the code above uses them. Yours come from your font.
Where to get the inputs:
- Ascent, descent, line gap and units per em are in the font file's tables. Tools that read them include fontkit, Capsize, the fontdrop.info website and the FontForge editor.
- Average character width can only be approximated. Averaging the widths of the letters
atozand the space is crude. Weighting them by how often each letter appears in your language is better. ThexAvgCharWidthvalue in the font's OS/2 table is another option, but subsetting tools often leave it out of date. - Ready-made values exist. Frameworks such as Next.js and Nuxt generate matching fallbacks automatically, and the Chrome article links a dataset with the overrides for fonts hosted by Google Fonts.
If you only want a smaller change, use the ascent, descent and line gap overrides on their own, set to the web font's metrics divided by its units per em. The Chrome team says this is simpler and usually enough to reduce the shifts noticeably. Adding size-adjust is what can remove them.
Choosing the local font
Arial is the usual choice for a sans-serif web font and Times New Roman for a serif, but neither is on Android, where Roboto is the system font. The numbers change with the fallback font, because the average width differs. For wide coverage, declare one adjusted fallback for Windows and Mac, a second for Android and a generic family at the end:
body { font-family: "Brand", "Brand Fallback", "Brand Fallback Android", sans-serif; }Do not fix this with a fixed line-height. The Chrome team advises against it. line-height sets the height of the line box and does nothing about the width of the letters or the mismatch of the fonts themselves.
Check the metrics for each system
Browsers on different systems read different tables from the font: hhea, typo or win. For most fonts, about 90% of those on Google Fonts according to Chrome, the values come out the same whichever table is used, so one set of overrides works everywhere. A font that gives different line heights on Windows and on Mac needs separate styles per system, which is only worth it if you can serve them.
Browser support
size-adjust works in Chrome 92 and later, Firefox 92 and later, and Safari 17 and later. The three vertical overrides work in Chrome 87 and Firefox 89 and are in preview in Safari at the time of writing. A browser that does not support a descriptor ignores it, so the worst case is the fallback as it is today.
Step 5: check the result
- Run the same test as in step 1: Layout Shift Regions with the cache disabled and the network throttled.
- Compare the first screen before and after the font arrives. The text should not re-wrap and the elements below should not move.
- Test on Windows, on a Mac and on an Android phone, because the local fonts differ.
- Watch the real-visitor CLS over the next weeks. See how to fix CLS for the other causes.
Other font problems that look like this
- A font loaded by a script after the page loads, so the swap happens late.
- An icon font that changes the width of buttons when it loads. Inline SVG avoids this.
- Different fonts for headings and body text, each with its own swap.
- A theme that loads the same family from two places.
Check your own page
Run the free audit and look at the Layout shift culprits and Font display insights. If a web font is named as a culprit, work through steps 2 to 4 for the fonts used above the fold first.
Sources
- Improved font fallbacks on Chrome for Developers: the equations, font tables, the effect of operating systems, and tools.
- font-display and size-adjust on MDN.
- Rendering tab and Network request blocking in Chrome DevTools.
Start here
Free website audit
Live Google data for any public page: mobile and desktop.
Mobile + desktop · Lighthouse lab test · Real-user Chrome data when Google has it
Rather have this done for you? Speed optimization · Book a call