Fix Web Fonts Causing Layout Shifts (FOIT/FOUT)

How to prevent layout shifts from web font loading using font-display, preloading, and fallback metrics.
Harlan WiltonHarlan Wilton6 min read Published

Text resizes when custom fonts load, shifting surrounding content. Only 11% of pages preload web fonts to prevent this.

What's the Problem?

When a web font loads, text rendered with the fallback font gets re-rendered with the new font. If the fonts have different metrics (size, line-height, letter-spacing), the text changes size and shifts surrounding content.

FOIT (Flash of Invisible Text): Text is hidden until font loads, then appears—can cause shift if layout changes.

FOUT (Flash of Unstyled Text): Fallback font shows first, then swaps to custom font—causes shift if metrics differ.

How to Identify This Issue

Chrome DevTools

  1. Network tab → filter by "Font"
  2. Throttle to "Slow 3G"
  3. Reload and watch for text reflow

Visual Test

Disable caching (DevTools → Network → Disable cache) and reload. Watch the text as fonts load.

The Fix

1. Use font-display: optional

The safest option for CLS—uses fallback permanently if font doesn't load fast:

@font-face {
  font-family: 'CustomFont';
  src: url('font.woff2') format('woff2');
  font-display: optional; /* No layout shift guaranteed */
}

How font-display values work:

ValueBehaviorCLS Risk
autoBrowser decidesHigh
blockHide text up to 3s, then swapMedium
swapShow fallback immediately, swap when readyHigh
fallbackShort block (100ms), then fallback, swap if fastLow
optionalVery short block, no swap if slowNone

2. Match Fallback Font Metrics

Adjust your fallback font to match the custom font's metrics:

@font-face {
  font-family: 'Adjusted Fallback';
  src: local('Arial');
  size-adjust: 105%;
  ascent-override: 95%;
  descent-override: 22%;
  line-gap-override: 0%;
}

body {
  font-family: 'CustomFont', 'Adjusted Fallback', sans-serif;
}

Use tools to generate these values:

3. Preload Critical Fonts

Tell the browser to download fonts early:

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

Important: The crossorigin attribute is required even for same-origin fonts, or the browser will download the font twice.

4. Self-Host Fonts

Google Fonts and other CDNs add extra connection time. Self-hosting is faster:

/* Instead of Google Fonts link */
@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter.woff2') format('woff2');
  font-display: optional;
}

Download fonts from Google Webfonts Helper or the font provider.

5. Subset Fonts

Only include characters you need:

pyftsubset font.ttf --text="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" --output-file=font-subset.woff2 --flavor=woff2

Smaller font files load faster, reducing the window for layout shifts.

Framework-Specific Solutions

Next.jsUse next/font for automatic optimization and zero CLS:
import { Inter } from 'next/font/google'

const inter = Inter({
  subsets: ['latin'],
  display: 'optional',
})

export default function Layout({ children }) {
  return (
    <html className={inter.className}>
      <body>{children}</body>
    </html>
  )
}
next/font automatically:
  • Self-hosts fonts
  • Generates fallback metrics
  • Preloads fonts
NuxtUse @nuxt/fonts for automatic optimization:
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxt/fonts'],
  fonts: {
    families: [
      { name: 'Inter', provider: 'google' }
    ]
  }
})
Or use Fontaine for fallback metrics:
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxtjs/fontaine']
})
Vue/ViteUse vite-plugin-fonts or Fontaine:
// vite.config.ts
import { FontaineTransform } from 'fontaine'

export default {
  plugins: [
    FontaineTransform.vite({
      fallbacks: ['Arial', 'sans-serif']
    })
  ]
}

Verify the Fix

After implementing:

  1. Throttle network to Slow 3G
  2. Reload with cache disabled
  3. Watch for text reflow—there should be none
  4. Compare CLS scores before/after

Expected improvement: Fixing font CLS can improve your score by 0.05-0.2 depending on how much text you have.

Common Mistakes

  • Forgetting crossorigin on preload — Causes double download.
  • Too many font weights — Each weight is a separate file. Only load what you use.
  • Using font-display: swap without fallback matching — Swap guarantees a shift unless fallbacks match.

Often appears alongside:

Test Your Entire Site

Font loading behavior can vary by page. Unlighthouse scans your entire site and identifies pages with font-related CLS.