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 | 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: Browsers require the crossorigin attribute even for same-origin fonts to avoid downloading 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.
Large Layout Shifts
Diagnose and fix layout shifts caused by CSS reflow, font-size changes, unsized media, web fonts, and injected iframes. Reduce your CLS score.
Dynamic Content
Stop cookie banners, notifications, and lazy-loaded content from causing layout shifts with space reservation and transforms.