/ โ† CLS Debugger Fixes / CLS from Fonts
In this guide
  1. Why a font swap moves the page
  2. What each font-display value does to layout
  3. Proving the font is the culprit
  4. Making the fallback the same size
  5. Getting fonts to arrive before first paint
  6. Less obvious font shifts
  7. The fix, step by step
  8. Keeping font shifts from returning
  9. Questions and answers

Why a font swap moves the page

Every font has its own proportions. Two typefaces set at 16 pixels can differ noticeably in how wide each letter is, how tall the lower-case letters are, and how much space sits above and below a line. The CSS font-size is the same, but the text that results is not.

So when the browser paints a paragraph in the fallback and then repaints it in the web font, three things can change at once:

Any of these makes the text block change height, and whatever sits below it is pushed up or down. That movement is what the browser records as a layout shift. Because the body font is usually used in the header and the first paragraphs, the moving area tends to be large and near the top of the screen, which is exactly where CLS scoring hurts most.

โš ๏ธ Hiding text until the font arrives does not solve this. With a blocking strategy the invisible text still occupies the fallback font's space, so when it appears in the web font the same reflow happens. You just also get a moment of blank page.

Why it varies between visitors

The shift only happens if the font arrives after the first paint. Returning visitors who have the file cached, and people on fast connections, often never see it. Mobile users on a new visit do. That is why a quick desktop test can look clean while the Core Web Vitals report in Search Console shows a problem on mobile.

What each font-display value does to layout

The font-display descriptor inside @font-face controls what the browser shows while a font is downloading and whether it will swap later. Browsers use roughly the time windows below; exact timings are up to the browser.

ValueWhile loadingLate arrivalLayout shift risk
blockText hidden for up to about 3 secondsSwaps in whenever it arrivesHigh, plus invisible text
swapFallback shown straight awaySwaps in whenever it arrivesHigh unless the fallback is metric-matched
fallbackHidden for about 100 ms, then fallbackSwaps only within about 3 secondsMedium
optionalHidden for about 100 ms, then fallbackNever swaps on this page view; used next timeVery low
autoBrowser decides, usually like blockUsually swapsHigh

optional is the most stable choice: if the font is not ready almost immediately, the page keeps the fallback for that visit. The trade-off is that a first-time visitor on a slow connection may never see your brand font on the first page they open. For body text that is usually acceptable. For a logo-style display font it may not be, and there swap with a tuned fallback is the better pairing.

๐Ÿ’ก Google Fonts sets the value from the display parameter in the stylesheet address, for example &display=swap. If you change it in your own CSS but still load the Google stylesheet, the Google value wins for those faces.

Proving the font is the culprit

Text shifting is not always a font problem. A banner inserted above the paragraph moves text too. Before changing anything, confirm the timing lines up with the font download.

  1. Record a load. In Chrome DevTools open the Performance panel, set network throttling to a slow 4G profile and CPU to a 4x slowdown, tick the option to disable the cache in the Network panel, and record a reload. Look at the Layout shifts track. Font-related shifts usually list headings, paragraphs or navigation items as the moved nodes.
  2. Compare with the font arrival. In the same recording, find the font requests in the network track. If the shift lands in the same frame as a .woff2 file finishing, you have your answer.
  3. Block the fonts. In the Network panel, right-click a font request and choose to block that URL, then reload. If the shift disappears when the font can never arrive, the swap is the cause.
  4. Log the moment fonts finish. Paste this into the console before reloading to print when each face is ready, then compare with the shift times shown by the CLS Debugger.
document.fonts.addEventListener('loadingdone', (e) => {
  console.log(Math.round(performance.now()) + ' ms',
    e.fontfaces.map((f) => f.family + ' ' + f.weight));
});

If several fonts finish at different times, you may see several small shifts in a row: regular text first, then bold, then italic. Each one reflows the paragraphs that use it. That pattern is a strong hint to cut the number of files, which is the cheapest fix of all.

Making the fallback the same size

The most reliable fix keeps the swap but makes it harmless. You declare a second @font-face that points at a font already installed on the device, and use four descriptors to stretch it to the proportions of your web font:

@font-face {
  font-family: 'Harbour Sans';
  src: url('/fonts/harbour-sans-var.woff2') format('woff2');
  font-weight: 300 800;
  font-display: swap;
}
@font-face {
  font-family: 'Harbour Sans Fallback';
  src: local('Arial');
  size-adjust: 104.2%;
  ascent-override: 93%;
  descent-override: 24%;
  line-gap-override: 0%;
}
body {
  font-family: 'Harbour Sans', 'Harbour Sans Fallback', sans-serif;
}

The numbers above are placeholders. The correct values come from the web font's own metrics (units per em, ascender, descender, line gap and average character width) compared with the fallback's. Do not guess them by eye. Tools such as Capsize publish metrics for common fonts and can calculate the overrides, Fontaine generates the fallback rules at build time, and the Next.js font loader does it automatically for Google and local fonts.

The Android problem

Arial is present on Windows, macOS and iOS but not on most Android phones, where the default sans-serif is Roboto. If your only fallback face uses local('Arial'), it fails on Android and the browser falls through to the generic family with no adjustments. Add a second fallback tuned for Roboto and list both:

font-family: 'Harbour Sans', 'Harbour Sans Fallback Arial',
             'Harbour Sans Fallback Roboto', sans-serif;

Serif designs need the same treatment against Times New Roman, and on Android against the system serif.

๐Ÿ’ก Support for the override descriptors is good in Chromium browsers and Firefox. Safari support has arrived later and more partially, so check a current compatibility table. Where a descriptor is not supported it is simply ignored, so adding them never makes things worse.

Getting fonts to arrive before first paint

If the font file is ready when the text is first painted, there is nothing to swap. Several changes help it win that race.

Fewer, smaller files

Every weight and style is normally a separate file. A site using regular, medium, semibold, bold and italic of two families may request ten files, each able to trigger its own reflow. A single variable font covering the weight range, or simply using two weights, removes most of them. Subsetting to the characters you actually use (for example Latin only for an English site) and serving WOFF2 keeps each file small.

Preload what the first screen needs

<link rel="preload" href="/fonts/harbour-sans-var.woff2"
      as="font" type="font/woff2" crossorigin>

The crossorigin attribute is required even when the font is on your own domain, because fonts are always fetched in anonymous CORS mode. Without it the browser downloads the file twice and the preload is wasted. Preload one or two files at most; preloading every weight competes with the main image and can make LCP worse.

Self-host rather than chain requests

Loading from a third-party font service means the browser must connect to one host for the stylesheet, read it, then connect to another host for the files. Hosting the WOFF2 files on your own domain, with long cache lifetimes, removes those round trips. If you keep a hosted service, add preconnect hints for both hosts. Check the service's licence before self-hosting commercial fonts.

Less obvious font shifts

The fix, step by step

Step 1

Confirm the shift is a font swap

Run the CLS Debugger and note which elements shifted and when. Then record a throttled reload in the Chrome DevTools Performance panel with the cache disabled, and check whether each text shift lands in the same frame as a .woff2 request finishing. Block the font requests and reload: if the shift vanishes, the swap is the cause. Write down every family, weight and style the page requests, because you will need that list next.

Step 2

Cut the number of font files

Go through the list and remove weights and styles that are barely used, such as a light weight used for one caption or a second family used only in the footer. Replace several static weights with one variable font where your typeface offers one. Subset the files to the character ranges your content needs and serve them as WOFF2. Fewer files means fewer separate reflows and a better chance of everything arriving before first paint.

Step 3

Preload and self-host the critical fonts

Move the remaining files onto your own domain, if the licence allows, with a long cache lifetime. Add a <link rel="preload" as="font" type="font/woff2" crossorigin> in the head for the one or two files the first screen uses, usually the body font and the heading weight. Remove the old third-party stylesheet link so fonts are not loaded twice, and keep preloads to a minimum so they do not delay the main image.

Step 4

Choose a font-display value for each font

Set font-display in every @font-face rule, not just the first. For body text, optional gives the most stable layout because a late font is kept for the next page view instead of swapping in. For brand or heading fonts that must always appear, use swap and rely on the adjusted fallback from the next step. If you still load Google Fonts, change the display parameter in the stylesheet address.

Step 5

Build a metric-matched fallback

Create a fallback @font-face that uses local() to point at Arial, and another for Roboto on Android, then set size-adjust, ascent-override, descent-override and line-gap-override from your font's real metrics using a tool such as Capsize or Fontaine. Add the fallback names to your font-family stack straight after the web font. Compare the page with fonts blocked and unblocked; line breaks should now match closely.

Step 6

Retest on a cold cache and slow network

Run the CLS Debugger again and repeat the throttled DevTools recording with the cache disabled, on both a desktop and a mobile viewport. Font-related shifts should have disappeared or dropped to tiny values. Check a few templates that use different fonts, such as blog posts and product pages. Then watch the mobile Core Web Vitals report in Search Console over the next four weeks, as real-visitor data updates slowly.

Keeping font shifts from returning

For other causes on the same page, the CLS Debugger Fixes hub matches each symptom to its guide.

Questions and answers

1. Is font-display swap bad for CLS?

Not by itself, but it is the setting most likely to show a shift. With swap the fallback appears immediately and the web font replaces it whenever it arrives, so any size difference between the two fonts moves the text. It works well when paired with a fallback adjusted using size-adjust and the override descriptors, so the replacement takes up the same space. Without that adjustment, optional or fallback usually give a more stable layout on slow connections.

2. Does font-display optional mean my font will not show?

On a first visit over a slow connection it may not. The browser waits about 100 milliseconds; if the file has not arrived, it uses the fallback for the rest of that page view and does not swap. The file still finishes downloading and is cached, so the next page the visitor opens uses the web font. Preloading the file makes it far more likely to arrive in time even on the first page.

3. Why do I need crossorigin on a font preload for my own domain?

Browsers always fetch fonts using anonymous CORS mode, even when the font comes from the same site as the page. A preload has to match the later request exactly for the browser to reuse it. Without the crossorigin attribute the preload is made in a different mode, the browser treats it as a different request, and it downloads the font a second time. You pay for the file twice and gain nothing from preloading.

4. Where do I get the numbers for size-adjust and ascent-override?

They come from metrics stored inside the font files: the units per em, the ascender, descender and line gap, and the average width of characters. You compare those with the same values for the fallback font. Doing this by hand is error-prone, so use a tool. Capsize publishes metrics for many fonts and can generate the overrides, Fontaine produces fallback rules during your build, and the Next.js font loader creates them automatically.

5. Will hosting Google Fonts myself reduce layout shift?

Often, yes, because the font can start downloading sooner. Loading from the Google service needs a connection to one host for the stylesheet and a second host for the font files, and only after reading the stylesheet does the browser know which files to fetch. Self-hosting the WOFF2 files and preloading the main one removes those steps. The shift itself only disappears if the font then arrives before first paint or the fallback is metric-matched.

6. Can icon fonts cause layout shift?

Yes. Many icon fonts use ligatures, so the HTML contains a word such as menu or search that the font turns into a picture. Until the font loads, that word is shown as ordinary text and can be wide enough to push other items along or onto a new line. When the font arrives, the word collapses into a small icon and everything moves back. Fixed dimensions on the icon element help, and SVG icons remove the problem.

7. Why does my CLS only fail on mobile when fonts are the cause?

Font shifts depend on the file arriving after the first paint. Desktop tests usually run on fast connections with powerful processors, so the font is often ready before anything is drawn. Mobile visitors on a first visit, with a slower network and a slower phone, see text painted in the fallback first and then swapped. Real-user reports reflect those visits, which is why the mobile report can show a problem desktop testing never reproduces.

See whether fonts are moving your text

The CLS Debugger records each layout shift as the page loads, names the elements that moved, works out the cumulative score and ranks the shifts by impact, so you can see whether your headings and paragraphs are among them.

Open CLS Debugger Fixes โ†’
๐Ÿ’ฌ Got a problem?
Related Guides: All CLS Debugger Fixes  ·  CLS Debugger Guide  ·  CLS in React and Next.js  ·  CLS from Images  ·  Learning Hub

About aiwebpageseo

aiwebpageseo.com is a data-driven SEO and AEO (Answer Engine Optimisation) platform providing a free suite of technical SEO tools. Rather than relying on AI-theorised assumptions, the platform analyses live URL performance, delivering objective diagnostics, page speed metrics, CLS debugging, and site crawl data alongside actionable technical tutorials.