Skip to content

Fix: Next.js Font Not Loading — Custom Fonts Not Applying or Flashing

FixDevs · (Updated: )

Part of:  React & Frontend Errors

Quick Answer

How to fix Next.js font loading issues — next/font/google setup, CSS variable approach, local fonts, font-display settings, FOUT flash, and Tailwind CSS font integration.

The Problem

A Next.js app doesn’t apply the configured custom font:

// app/layout.tsx
import { Inter } from 'next/font/google';

const inter = Inter({ subsets: ['latin'] });

export default function RootLayout({ children }) {
  return (
    <html lang="en" className={inter.className}>
      <body>{children}</body>
    </html>
  );
}
// Font imports correctly but the site still shows the default system font

Or there’s a Flash of Unstyled Text (FOUT) — the page briefly shows a system font before the custom font loads:

Page loads → System font visible for 200-500ms → Custom font appears

Or a local font file isn’t being picked up:

Warning: Font file not found: ./fonts/CustomFont.woff2

Or in the Pages Router, the font setup works in production but not locally:

npm run dev
# Font works fine

npm run build && npm start
# Font doesn't apply in production

Why This Happens

Next.js has a built-in font optimization system (next/font) that downloads, self-hosts, and optimizes fonts at build time. Several things can go wrong:

  • Font class not applied to <html> or <body> — the font class must be on the outermost element. Applying it only to a child component limits the font to that subtree.
  • CSS specificity overriding the font — another CSS rule with higher specificity overrides font-family, ignoring the Next.js font class.
  • next/font used in a Client Componentnext/font must be used in Server Components or the root layout. It doesn’t work inside Client Components marked with 'use client'.
  • CSS variables not configured — when using variable mode, the CSS custom property must also be applied in globals.css or Tailwind config.
  • Local font path incorrect — the path to the font file in next/font/local is resolved relative to the file that imports it, not the project root.
  • Subset not specified — Google Fonts require specifying subsets or the build fails.

The behavior also differs between deployment environments. On Vercel, next/font/google downloads fonts through Vercel’s edge network at build time, so fonts are always available and served from the same origin. When self-hosting on a VPS or behind a reverse proxy, the build process fetches fonts from Google’s servers during next build. If the build server has no outbound internet access (common in air-gapped corporate environments or locked-down Docker builds), the font download silently fails and the app falls back to system fonts without any build error.

Fix 1: Apply the Font Class Correctly

The font class or CSS variable must be applied to the root <html> element:

// app/layout.tsx (App Router)
import { Inter, Roboto_Mono } from 'next/font/google';

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',   // Prevent layout shift — show fallback font while loading
});

const robotoMono = Roboto_Mono({
  subsets: ['latin'],
  display: 'swap',
  variable: '--font-roboto-mono',  // CSS variable for use elsewhere
});

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    // Apply BOTH className AND variable — className sets the default font
    <html lang="en" className={`${inter.className} ${robotoMono.variable}`}>
      <body>{children}</body>
    </html>
  );
}

Pages Router — apply in _app.tsx:

// pages/_app.tsx
import { Inter } from 'next/font/google';
import type { AppProps } from 'next/app';
import '../styles/globals.css';

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

export default function App({ Component, pageProps }: AppProps) {
  return (
    <main className={inter.className}>
      <Component {...pageProps} />
    </main>
  );
}

Verify the font class is in the HTML:

# Build and check the rendered HTML
npm run build && npm start

# Inspect the HTML — should see the class on <html>
curl http://localhost:3000 | grep -o 'class="[^"]*"' | head -5
# <html class="__className_abc123 __variable_def456">

Fix 2: Use CSS Variables with Tailwind

The most flexible approach — expose the font as a CSS variable, then use it in Tailwind:

// app/layout.tsx
import { Inter, Merriweather } from 'next/font/google';

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',
  variable: '--font-inter',        // CSS custom property name
});

const merriweather = Merriweather({
  weight: ['300', '400', '700'],
  subsets: ['latin'],
  display: 'swap',
  variable: '--font-merriweather',
});

export default function RootLayout({ children }) {
  return (
    <html lang="en" className={`${inter.variable} ${merriweather.variable}`}>
      <body>{children}</body>
    </html>
  );
}
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ['./app/**/*.{js,ts,jsx,tsx}'],
  theme: {
    extend: {
      fontFamily: {
        sans: ['var(--font-inter)', 'system-ui', 'sans-serif'],
        serif: ['var(--font-merriweather)', 'Georgia', 'serif'],
      },
    },
  },
};
// Usage in components
<h1 className="font-serif text-3xl">Heading in Merriweather</h1>
<p className="font-sans text-base">Body text in Inter</p>

Tailwind CSS v4 — use CSS @theme instead:

/* app/globals.css */
@import "tailwindcss";

@theme {
  --font-family-sans: var(--font-inter), system-ui, sans-serif;
  --font-family-serif: var(--font-merriweather), Georgia, serif;
  --font-family-mono: var(--font-roboto-mono), ui-monospace, monospace;
}

Fix 3: Use Local Fonts Correctly

next/font/local loads fonts from your project’s files. The path is relative to the file that imports it:

project/
├── app/
│   ├── layout.tsx      ← imports the font
│   └── globals.css
└── public/
    └── fonts/
        ├── CustomFont-Regular.woff2
        └── CustomFont-Bold.woff2
// app/layout.tsx
import localFont from 'next/font/local';

// Path is relative to layout.tsx
const customFont = localFont({
  src: [
    {
      path: '../public/fonts/CustomFont-Regular.woff2',
      weight: '400',
      style: 'normal',
    },
    {
      path: '../public/fonts/CustomFont-Bold.woff2',
      weight: '700',
      style: 'normal',
    },
  ],
  display: 'swap',
  variable: '--font-custom',
});

Alternative — put fonts in the app/ directory:

project/
└── app/
    ├── layout.tsx
    └── fonts/
        ├── CustomFont-Regular.woff2
        └── CustomFont-Bold.woff2
// app/layout.tsx — path relative to this file
const customFont = localFont({
  src: './fonts/CustomFont-Regular.woff2',  // Simpler path
  display: 'swap',
});

Multiple formats for browser compatibility:

const customFont = localFont({
  src: [
    {
      path: './fonts/CustomFont.woff2',
      weight: '400',
    },
    // woff2 is sufficient for all modern browsers
    // Only add woff/ttf if supporting IE11 or very old browsers
  ],
});

Fix 4: Fix Font Not Working in Client Components

next/font instances must be created at module level in a Server Component. They can’t be created inside Client Components:

// WRONG — font in a Client Component
'use client';

import { Inter } from 'next/font/google';

// Error: next/font must be used in a Server Component
const inter = Inter({ subsets: ['latin'] });

export default function Header() {
  return <header className={inter.className}>...</header>;
}
// CORRECT — font in layout.tsx (Server Component), passed via className or CSS variable

// app/layout.tsx (Server Component)
import { Inter } from 'next/font/google';
import './globals.css';

const inter = Inter({
  subsets: ['latin'],
  variable: '--font-inter',
});

export default function RootLayout({ children }) {
  return (
    <html lang="en" className={inter.variable}>
      <body>{children}</body>
    </html>
  );
}

// app/globals.css — apply the variable
body {
  font-family: var(--font-inter), sans-serif;
}
// Client Component — uses the CSS variable (no next/font import needed)
'use client';

export default function Header() {
  // CSS variable --font-inter is already set on <html>
  // font-family is inherited from body
  return <header>...</header>;
}

Fix 5: Fix FOUT and Font Display

Flash of Unstyled Text occurs when the font loads after the content is visible. Control this with font-display:

// Options for display parameter:
const inter = Inter({
  subsets: ['latin'],
  display: 'swap',       // Show fallback font, swap to custom font when ready
  // display: 'block'    // Hide text until font loads (FOIT — may cause invisible text)
  // display: 'optional' // Only use custom font if already cached
  // display: 'fallback' // Short block period, then swap if ready, else use fallback
  // display: 'auto'     // Browser default behavior
});

Configure a fallback font for minimal layout shift:

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',
  fallback: ['Helvetica Neue', 'Arial', 'sans-serif'],  // Fallback font stack
  adjustFontFallback: true,  // Next.js adjusts fallback metrics to minimize layout shift
});

adjustFontFallback: true (default for Google Fonts) tells Next.js to generate CSS that adjusts the fallback font’s metrics (size, line height, letter spacing) to match the custom font’s metrics as closely as possible, minimizing layout shift during the swap.

Safari vs Chrome rendering differences: Safari and Chrome handle font-display: swap differently. Chrome shows the fallback font immediately and swaps to the custom font as soon as it loads. Safari sometimes delays the swap slightly, resulting in a longer visible flash. Safari also caches fonts more aggressively in its disk cache, so after the first visit, FOUT usually disappears entirely on Safari but may persist on Chrome if the cache headers are short. To minimize cross-browser FOUT differences, use display: 'optional' for non-critical fonts — this tells both browsers to only use the custom font if it loads within the first ~100ms, eliminating the flash entirely at the cost of occasionally showing the system font.

Fix 6: Deployment and Build Environment Issues

Font loading failures often depend on the deployment target and build environment rather than the code itself.

Vercel edge vs self-hosted:

On Vercel, next/font/google works seamlessly because Vercel’s build infrastructure has direct access to Google Fonts and caches the downloaded font files as part of the deployment artifact. When self-hosting, the next build step downloads fonts from fonts.googleapis.com during the build. If your build server sits behind a corporate firewall or proxy that blocks outbound HTTPS, the download fails silently and the font falls back to system fonts.

# Test if the build server can reach Google Fonts
curl -I https://fonts.googleapis.com
# If this times out, use next/font/local instead

Docker offline builds:

Docker builds in CI pipelines often run without internet access after the npm install step. If next/font/google tries to download fonts during next build and the network is unavailable, the build either fails or produces a bundle without the custom font.

# Solution 1: Download fonts during the install step (network available)
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci

# Solution 2: Use next/font/local with font files committed to the repo
# This eliminates the runtime font download entirely
// Switch from next/font/google to next/font/local for offline-compatible builds
import localFont from 'next/font/local';

// Download the font file manually from fonts.google.com and commit to repo
const inter = localFont({
  src: './fonts/Inter-Variable.woff2',
  display: 'swap',
  variable: '--font-inter',
});

CI font download timeouts:

GitHub Actions and GitLab CI runners sometimes experience intermittent timeouts when downloading Google Fonts during next build. The build succeeds but the font is missing from the output. Add a retry mechanism or pre-download fonts in a separate step:

# GitHub Actions — pre-download fonts before build
- name: Build Next.js
  run: npm run build
  env:
    # Increase the timeout for font downloads (default is 5s)
    NEXT_FONT_GOOGLE_TIMEOUT: 30000

Fix 7: Preload Critical Fonts

For fonts used above the fold, add preload to reduce FOUT:

// next/font automatically adds preload links for the fonts it generates
// You can verify in the HTML <head>:
// <link rel="preload" href="/_next/static/media/..." as="font" type="font/woff2" crossorigin>

// To control preloading behavior:
const inter = Inter({
  subsets: ['latin'],
  preload: true,    // Default — adds preload link for this font
  // preload: false // Disable preloading (useful for fonts only used below the fold)
});

Verify the preload link is in the HTML:

curl -s http://localhost:3000 | grep 'rel="preload"'
# <link rel="preload" href="/_next/static/media/abc123.woff2" as="font" type="font/woff2" crossorigin="anonymous">

Fix 8: Debug Font Issues

When the font isn’t applying, systematically check each step:

# 1. Verify the font CSS class is in the HTML
curl -s http://localhost:3000 | grep -E 'class="[^"]*"' | head -3

# 2. Check the network tab for font requests
# Open DevTools → Network → Filter "Font" — font files should appear as 200 responses

# 3. Verify the font is self-hosted (not loaded from Google Fonts)
# next/font should load fonts from /_next/static/media/ — NOT from fonts.googleapis.com
curl -s http://localhost:3000 | grep "googleapis"
# Should output nothing — next/font self-hosts

# 4. Check the computed font in the browser
# DevTools → Elements → select <body> → Computed → font-family
# Should show your custom font name

CSS overriding the font — check specificity:

/* If another stylesheet sets font-family with high specificity, it overrides next/font */

/* PROBLEMATIC — may override your font */
body {
  font-family: 'Times New Roman', serif !important;
}

/* Check in globals.css or any imported CSS for font-family rules */
/* The next/font className applies font-family via:
   .__className_abc123 { font-family: 'Inter', ... }
   This has one class specificity — easy to override */

Force the font in globals.css as a safety net:

/* app/globals.css */
body {
  font-family: var(--font-inter), system-ui, sans-serif;
  /* Using the CSS variable is more robust than relying solely on the class */
}

Still Not Working?

Font subsets missing characters — if you load only the latin subset but display non-Latin characters, those characters fall back to the system font. Add the required subset:

const inter = Inter({
  subsets: ['latin', 'latin-ext', 'cyrillic'],  // Add needed subsets
});

Build cache stale — after changing font configuration, clear the Next.js cache:

rm -rf .next
npm run build

next.config.js font optimization disabled — ensure font optimization isn’t explicitly disabled:

// next.config.js
module.exports = {
  optimizeFonts: true,   // Should be true (default) — don't disable
};

Using @font-face directly — if you’re defining fonts with @font-face in CSS (not using next/font), Next.js won’t optimize them. Migrate to next/font/local for built-in optimization.

next/font/google vs next/font/local — when to use which: Use next/font/google when deploying to Vercel or any environment with reliable internet during build. Use next/font/local when building in Docker without network access, deploying to air-gapped environments, or when you need deterministic builds that don’t depend on external services. next/font/local also works for commercial fonts that aren’t available on Google Fonts.

Font works in dev but not productionnext dev loads fonts differently than next build && next start. In dev mode, fonts may be fetched on-demand from Google’s CDN. In production, they should be self-hosted from /_next/static/media/. If the production build doesn’t include the font files, check that the build completed without network errors and clear the .next cache.

For related Next.js issues, see Fix: Next.js Hydration Failed, Fix: Next.js Build Failed, Fix: Next.js Image Optimization Error, and Fix: Next.js Env Variables Not Working.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles