---
title: "Fix Web Fonts Causing Layout Shifts (FOIT/FOUT)"
description: "How to prevent layout shifts from web font loading using font-display, preloading, and fallback metrics."
canonical_url: "https://unlighthouse.dev/learn-lighthouse/cls/web-fonts-causing-foit"
last_updated: "2025-01-12"
---

Text resizes when custom fonts load, shifting surrounding content. [Only 11% of pages preload web fonts](https://almanac.httparchive.org/en/2024/performance) 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:

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

**How font-display values work:**

<table>
<thead>
  <tr>
    <th>
      Value
    </th>
    
    <th>
      Behavior
    </th>
    
    <th>
      CLS Risk
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        auto
      </code>
    </td>
    
    <td>
      Browser decides
    </td>
    
    <td>
      High
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        block
      </code>
    </td>
    
    <td>
      Hide text up to 3s, then swap
    </td>
    
    <td>
      Medium
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        swap
      </code>
    </td>
    
    <td>
      Show fallback immediately, swap when ready
    </td>
    
    <td>
      High
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        fallback
      </code>
    </td>
    
    <td>
      Short block (100ms), then fallback, swap if fast
    </td>
    
    <td>
      Low
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        optional
      </code>
    </td>
    
    <td>
      short block, no swap if slow
    </td>
    
    <td>
      None
    </td>
  </tr>
</tbody>
</table>

### 2. match fallback font metrics

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

```css
@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](https://github.com/unjs/fontaine) - Automatic fallback generation
- [Font Style Matcher](https://meowni.ca/font-style-matcher/) - Visual comparison tool

### 3. preload critical fonts

Tell the browser to download fonts early:

```html
<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:

```css
/* 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](https://gwfh.mranftl.com/fonts) or the font provider.

### 5. subset fonts

Only include characters you need:

```bash
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

<callout icon="i-logos-nextjs-icon">

**Next.js**

Use `next/font` for automatic optimization and zero CLS:

```jsx
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

</callout>

<callout icon="i-logos-nuxt-icon">

**Nuxt**

Use `@nuxt/fonts` for automatic optimization:

```ts
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxt/fonts'],
  fonts: {
    families: [
      { name: 'Inter', provider: 'google' }
    ]
  }
})
```

Or use Fontaine for fallback metrics:

```ts
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxtjs/fontaine']
})
```

</callout>

<callout icon="i-logos-vue">

**Vue/Vite**

Use `vite-plugin-fonts` or Fontaine:

```ts
// vite.config.ts
import { FontaineTransform } from 'fontaine'

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

</callout>

## 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.

## Related issues

Often appears alongside:

- [Unsized Images](/learn-lighthouse/cls/unsized-images) - Multiple small shifts add up
- [Dynamic Content](/learn-lighthouse/cls/dynamic-content-injection) - 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.
