Skip to content

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

FixDevs ·

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.

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.

Fix 6: 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 7: 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.

For related Next.js issues, see Fix: Next.js Fetch Cache Not Working and Fix: Next.js API Route 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