Fix Web Fonts Causing Layout Shifts (FOIT/FOUT)
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
- Network tab → filter by "Font"
- Throttle to "Slow 3G"
- 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:
| Value | Behavior | CLS Risk |
|---|---|---|
auto | Browser decides | High |
block | Hide text up to 3s, then swap | Medium |
swap | Show fallback immediately, swap when ready | High |
fallback | Short block (100ms), then fallback, swap if fast | Low |
optional | Very short block, no swap if slow | None |
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:
- Fontaine — Automatic fallback generation
- Font Style Matcher — Visual comparison tool
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/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
@nuxt/fonts for automatic optimization:// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxt/fonts'],
fonts: {
families: [
{ name: 'Inter', provider: 'google' }
]
}
})
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxtjs/fontaine']
})
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:
- Throttle network to Slow 3G
- Reload with cache disabled
- Watch for text reflow—there should be none
- 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.
Related Issues
Often appears alongside:
- Unsized Images — Multiple small shifts add up
- Dynamic Content — Fonts loading in dynamic content
Test Your Entire Site
Font loading behavior can vary by page. Unlighthouse scans your entire site and identifies pages with font-related CLS.
Related
Large Layout Shifts
Identify and eliminate the biggest layout shifts on your page. Diagnose root causes including unsized media, web fonts, and injected iframes.
Dynamic Content
Stop cookie banners, notifications, and lazy-loaded content from causing layout shifts with space reservation and transforms.