NextJS DOC
Part 12 of 151. Next.js Project Structure: A Comprehensive Architecture Guide2. Next.js Layouts and Pages: Complete Architecture Guide3. Next.js Linking and Navigation: Complete Architecture Guide4. Next.js Server and Client Components: Complete Architecture Guide5. Next.js Data Fetching: Complete Architecture Guide6. Next.js Data Mutation: Complete Server Actions Guide7. Next.js Caching Deep Dive: Cache Components and the `use cache` Directive8. Next.js Revalidation Deep Dive: Time-Based and On-Demand Cache Invalidation9. Next.js Error Handling Deep Dive: Expected Errors, Uncaught Exceptions, and Recovery Patterns10. Next.js CSS Deep Dive: Tailwind, CSS Modules, Sass, and CSS-in-JS11. Next.js Image Optimization Deep Dive: Performance, Responsive Images, and Configuration12. Next.js Font Optimization Deep Dive: Self-Hosted Fonts, CSS Variables, and CLS Prevention13. Next.js Metadata and OG Images Deep Dive: SEO, Social Sharing, and Dynamic Generation14. Next.js Route Handlers Deep Dive: Building Production APIs with Web Standards15. Next.js Proxy Deep Dive: Edge-First Request Interception
Next.js Font Optimization Deep Dive: Self-Hosted Fonts, CSS Variables, and CLS Prevention
April 3, 202672 min read0 views
Next.js Font Optimization Deep Dive: Self-Hosted Fonts, CSS Variables, and CLS Prevention
Introduction
The next/font module provides automatic font optimization with built-in self-hosting, eliminating external network requests to Google Fonts and preventing Cumulative Layout Shift (CLS). This guide covers font loading strategies, configuration options, and production patterns for optimal typography performance.
Font Optimization Architecture
┌─────────────────────────────────────────────────────────────────────────────┐
│ NEXT.JS FONT OPTIMIZATION PIPELINE │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ BUILD TIME PROCESSING │ │
│ │ │ │
│ │ import { Inter } from 'next/font/google' │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌──────────────────────────────────────────────────────────────┐ │ │
│ │ │ 1. Download font files from Google Fonts │ │ │
│ │ │ 2. Subset font based on 'subsets' option │ │ │
│ │ │ 3. Generate optimized font files (woff2) │ │ │
│ │ │ 4. Calculate font metrics (ascent, descent, lineGap) │ │ │
│ │ │ 5. Generate fallback font CSS (adjustFontFallback) │ │ │
│ │ │ 6. Store in .next/static/media/ │ │ │
│ │ └──────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ RUNTIME BEHAVIOR │ │
│ │ │ │
│ │ HTML Output: │ │
│ │ ┌──────────────────────────────────────────────────────────────┐ │ │
│ │ │ <html class="__className_a3b2c1"> │ │ │
│ │ │ <head> │ │ │
│ │ │ <link rel="preload" │ │ │
│ │ │ href="/_next/static/media/inter-latin.woff2" │ │ │
│ │ │ as="font" │ │ │
│ │ │ type="font/woff2" │ │ │
│ │ │ crossorigin="anonymous" /> │ │ │
│ │ │ <style> │ │ │
│ │ │ @font-face { │ │ │
│ │ │ font-family: '__Inter_a3b2c1'; │ │ │
│ │ │ font-display: swap; │ │ │
│ │ │ src: url(/_next/static/media/inter.woff2); │ │ │
│ │ │ } │ │ │
│ │ │ .__className_a3b2c1 { │ │ │
│ │ │ font-family: '__Inter_a3b2c1', '__Inter_Fallback'; │ │ │
│ │ │ } │ │ │
│ │ │ </style> │ │ │
│ │ │ </head> │ │ │
│ │ │ </html> │ │ │
│ │ └──────────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ CLS PREVENTION │ │
│ │ │ │
│ │ adjustFontFallback generates metrics-matched fallback: │ │
│ │ ┌──────────────────────────────────────────────────────────────┐ │ │
│ │ │ @font-face { │ │ │
│ │ │ font-family: '__Inter_Fallback'; │ │ │
│ │ │ src: local('Arial'); │ │ │
│ │ │ ascent-override: 90.49%; │ │ │
│ │ │ descent-override: 22.56%; │ │ │
│ │ │ line-gap-override: 0%; │ │ │
│ │ │ size-adjust: 107.06%; │ │ │
│ │ │ } │ │ │
│ │ └──────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ Result: Fallback font matches target font metrics │ │
│ │ → No layout shift when web font loads │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Google Fonts
Basic Usage
// app/layout.tsx
import { Inter } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
display: 'swap',
})
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en" className={inter.className}>
<body>{children}</body>
</html>
)
}
Variable Fonts (Recommended)
Variable fonts provide all weights in a single file, reducing network requests:
import { Inter } from 'next/font/google'
// Variable font - no weight specification needed
const inter = Inter({
subsets: ['latin'],
display: 'swap',
})
// Use any weight in CSS
// font-weight: 100 to 900
Static Fonts (Non-Variable)
import { Roboto } from 'next/font/google'
// Non-variable font - weight required
const roboto = Roboto({
weight: ['400', '700'],
style: ['normal', 'italic'],
subsets: ['latin'],
display: 'swap',
})
Multi-Word Font Names
import { Roboto_Mono, Open_Sans, Playfair_Display } from 'next/font/google'
// Use underscore for multi-word font names
const robotoMono = Roboto_Mono({ subsets: ['latin'] })
const openSans = Open_Sans({ subsets: ['latin'] })
const playfair = Playfair_Display({ subsets: ['latin'] })
Local Fonts
Single Font File
// app/layout.tsx
import localFont from 'next/font/local'
const myFont = localFont({
src: './fonts/MyFont.woff2',
display: 'swap',
})
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en" className={myFont.className}>
<body>{children}</body>
</html>
)
}
Multiple Font Files (Font Family)
import localFont from 'next/font/local'
const roboto = localFont({
src: [
{
path: './fonts/Roboto-Regular.woff2',
weight: '400',
style: 'normal',
},
{
path: './fonts/Roboto-Italic.woff2',
weight: '400',
style: 'italic',
},
{
path: './fonts/Roboto-Bold.woff2',
weight: '700',
style: 'normal',
},
{
path: './fonts/Roboto-BoldItalic.woff2',
weight: '700',
style: 'italic',
},
],
display: 'swap',
})
Variable Local Font
import localFont from 'next/font/local'
const inter = localFont({
src: './fonts/InterVariable.woff2',
weight: '100 900', // Variable font weight range
style: 'normal',
display: 'swap',
})
Configuration Options
Complete Options Reference
import { Inter } from 'next/font/google'
import localFont from 'next/font/local'
// Google Font - all options
const inter = Inter({
// Required for Google Fonts
subsets: ['latin', 'latin-ext'], // Preload these subsets
// Weight options
weight: '400', // Single weight
weight: ['400', '700'], // Multiple weights (non-variable)
weight: '100 900', // Weight range (variable fonts)
// Style options
style: 'normal', // Default
style: ['normal', 'italic'], // Multiple styles
// Display strategy
display: 'swap', // Default - best for CLS
display: 'block', // Hide text until font loads
display: 'fallback', // Short block, then fallback
display: 'optional', // May not load at all
// Variable font axes
axes: ['slnt', 'opsz'], // Additional variable axes
// Preloading
preload: true, // Default - preload font files
// Fallback fonts
fallback: ['system-ui', 'arial'], // CSS fallback stack
// CLS prevention
adjustFontFallback: true, // Generate metrics-matched fallback
// CSS variable
variable: '--font-inter', // Define CSS custom property
})
// Local Font - additional options
const myFont = localFont({
src: './fonts/MyFont.woff2',
// Fallback adjustment
adjustFontFallback: 'Arial', // Use Arial metrics for fallback
adjustFontFallback: 'Times New Roman',
adjustFontFallback: false, // Disable fallback adjustment
// Custom @font-face declarations
declarations: [
{ prop: 'ascent-override', value: '90%' },
{ prop: 'descent-override', value: '10%' },
{ prop: 'unicode-range', value: 'U+0000-00FF' },
],
})
font-display Strategies
┌─────────────────────────────────────────────────────────────────────────────┐
│ FONT-DISPLAY STRATEGIES │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ SWAP (Default - Recommended) │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ • Fallback font shows immediately │ │
│ │ • Swaps to web font when loaded (may cause FOUT) │ │
│ │ • Best for body text - content always visible │ │
│ │ • No impact on FCP (First Contentful Paint) │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │
│ BLOCK │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ • Invisible text for short period (3s typically) │ │
│ │ • Then shows web font or fallback │ │
│ │ • Use for icon fonts or where fallback makes no sense │ │
│ │ • Can hurt FCP │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │
│ FALLBACK │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ • Very short block period (~100ms) │ │
│ │ • Shows fallback, swaps only if font loads quickly │ │
│ │ • Reduces FOUT on slower connections │ │
│ │ • Good balance for headings │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │
│ OPTIONAL │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ • Very short block period │ │
│ │ • Only uses web font if already cached │ │
│ │ • Never causes FOUT - no layout shift │ │
│ │ • Font may never display on first visit │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Applying Fonts
Method 1: className
import { Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'] })
// Apply to element
<div className={inter.className}>
<p>This text uses Inter font</p>
</div>
// Apply to html element (global)
<html className={inter.className}>
<body>{children}</body>
</html>
Method 2: style Object
import { Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'] })
// Apply font-family directly
<p style={inter.style}>This text uses Inter font</p>
// Access specific properties
console.log(inter.style.fontFamily)
// "__Inter_a3b2c1, __Inter_Fallback"
Method 3: CSS Variables (Most Flexible)
// app/layout.tsx
import { Inter, Roboto_Mono } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
variable: '--font-inter',
})
const robotoMono = Roboto_Mono({
subsets: ['latin'],
variable: '--font-mono',
})
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en" className={`${inter.variable} ${robotoMono.variable}`}>
<body>{children}</body>
</html>
)
}
/* app/globals.css */
body {
font-family: var(--font-inter);
}
code, pre {
font-family: var(--font-mono);
}
h1, h2, h3 {
font-family: var(--font-inter);
font-weight: 700;
}
Tailwind CSS Integration
Tailwind v4
// app/layout.tsx
import { Inter, JetBrains_Mono } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
variable: '--font-inter',
})
const jetbrainsMono = JetBrains_Mono({
subsets: ['latin'],
variable: '--font-mono',
})
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html
lang="en"
className={`${inter.variable} ${jetbrainsMono.variable} antialiased`}
>
<body>{children}</body>
</html>
)
}
/* app/globals.css */
@import 'tailwindcss';
@theme inline {
--font-sans: var(--font-inter);
--font-mono: var(--font-mono);
}
Tailwind v3
// tailwind.config.js
module.exports = {
theme: {
extend: {
fontFamily: {
sans: ['var(--font-inter)', 'system-ui', 'sans-serif'],
mono: ['var(--font-mono)', 'monospace'],
},
},
},
}
// Usage in components
<p className="font-sans">Body text with Inter</p>
<code className="font-mono">Code with JetBrains Mono</code>
Production Patterns
Pattern 1: Font Definitions File
// lib/fonts.ts
import { Inter, Roboto_Mono, Playfair_Display } from 'next/font/google'
import localFont from 'next/font/local'
// Primary body font
export const inter = Inter({
subsets: ['latin'],
variable: '--font-inter',
display: 'swap',
})
// Code font
export const robotoMono = Roboto_Mono({
subsets: ['latin'],
variable: '--font-mono',
display: 'swap',
})
// Display/heading font
export const playfair = Playfair_Display({
subsets: ['latin'],
variable: '--font-display',
display: 'swap',
})
// Brand font (local)
export const brandFont = localFont({
src: [
{ path: '../fonts/Brand-Regular.woff2', weight: '400' },
{ path: '../fonts/Brand-Bold.woff2', weight: '700' },
],
variable: '--font-brand',
display: 'swap',
})
// app/layout.tsx
import { inter, robotoMono, playfair, brandFont } from '@/lib/fonts'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html
lang="en"
className={`
${inter.variable}
${robotoMono.variable}
${playfair.variable}
${brandFont.variable}
`}
>
<body className="font-sans">{children}</body>
</html>
)
}
Pattern 2: Component-Scoped Fonts
// components/CodeBlock.tsx
import { JetBrains_Mono } from 'next/font/google'
const jetbrainsMono = JetBrains_Mono({
subsets: ['latin'],
display: 'swap',
})
interface CodeBlockProps {
children: string
language?: string
}
export function CodeBlock({ children, language }: CodeBlockProps) {
return (
<pre className={`${jetbrainsMono.className} bg-gray-900 p-4 rounded-lg`}>
<code data-language={language}>{children}</code>
</pre>
)
}
Pattern 3: Typography Scale with CSS Variables
// app/layout.tsx
import { Inter, Playfair_Display } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
variable: '--font-body',
})
const playfair = Playfair_Display({
subsets: ['latin'],
variable: '--font-heading',
})
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en" className={`${inter.variable} ${playfair.variable}`}>
<body>{children}</body>
</html>
)
}
/* app/globals.css */
:root {
--font-size-xs: 0.75rem;
--font-size-sm: 0.875rem;
--font-size-base: 1rem;
--font-size-lg: 1.125rem;
--font-size-xl: 1.25rem;
--font-size-2xl: 1.5rem;
--font-size-3xl: 1.875rem;
--font-size-4xl: 2.25rem;
--font-size-5xl: 3rem;
}
body {
font-family: var(--font-body);
font-size: var(--font-size-base);
line-height: 1.6;
}
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-heading);
line-height: 1.2;
}
h1 { font-size: var(--font-size-5xl); }
h2 { font-size: var(--font-size-4xl); }
h3 { font-size: var(--font-size-3xl); }
h4 { font-size: var(--font-size-2xl); }
.text-sm { font-size: var(--font-size-sm); }
.text-lg { font-size: var(--font-size-lg); }
.text-xl { font-size: var(--font-size-xl); }
Pattern 4: Conditional Font Loading
// components/LanguageSpecificText.tsx
import { Noto_Sans_JP, Noto_Sans_KR, Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'] })
const notoSansJP = Noto_Sans_JP({ subsets: ['latin'] })
const notoSansKR = Noto_Sans_KR({ subsets: ['latin'] })
const fonts = {
en: inter,
ja: notoSansJP,
ko: notoSansKR,
}
interface LanguageSpecificTextProps {
locale: 'en' | 'ja' | 'ko'
children: React.ReactNode
}
export function LanguageSpecificText({
locale,
children,
}: LanguageSpecificTextProps) {
const font = fonts[locale]
return <span className={font.className}>{children}</span>
}
Pattern 5: Icon Font with Local Font
// lib/fonts/icons.ts
import localFont from 'next/font/local'
export const iconFont = localFont({
src: './icons.woff2',
variable: '--font-icons',
display: 'block', // Block for icon fonts
preload: true,
declarations: [
{ prop: 'font-weight', value: 'normal' },
{ prop: 'font-style', value: 'normal' },
],
})
/* styles/icons.css */
.icon {
font-family: var(--font-icons);
font-style: normal;
font-weight: normal;
speak: none;
display: inline-block;
text-decoration: inherit;
text-align: center;
font-variant: normal;
text-transform: none;
line-height: 1;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-home::before { content: '\e800'; }
.icon-user::before { content: '\e801'; }
.icon-settings::before { content: '\e802'; }
Performance Optimization
Subset Selection
// Only preload necessary subsets
const inter = Inter({
// Minimal subset for English-only sites
subsets: ['latin'],
// Extended Latin for European languages
// subsets: ['latin', 'latin-ext'],
// Multiple subsets for multilingual sites
// subsets: ['latin', 'cyrillic', 'greek'],
})
Preload Control
// Global font - preload on all routes
// (defined in root layout)
const globalFont = Inter({
subsets: ['latin'],
preload: true, // Default
})
// Route-specific font - may not want to preload globally
// (defined in specific page/layout)
const specialFont = Playfair_Display({
subsets: ['latin'],
preload: false, // Don't preload globally
})
Variable Font Axes
import { Inter } from 'next/font/google'
// Only include weight axis (default, smallest file)
const interMinimal = Inter({
subsets: ['latin'],
})
// Include slant axis for variable italics
const interWithSlant = Inter({
subsets: ['latin'],
axes: ['slnt'], // Adds ~10% to file size
})
CLS Prevention
How adjustFontFallback Works
┌─────────────────────────────────────────────────────────────────────────────┐
│ FALLBACK FONT METRIC ADJUSTMENT │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ WITHOUT adjustFontFallback: │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ 1. Text renders with Arial (fallback) │ │
│ │ 2. Inter loads │ │
│ │ 3. Text re-renders with Inter │ │
│ │ 4. Different metrics = LAYOUT SHIFT │ │
│ │ │ │
│ │ ┌─────────────────────┐ ┌─────────────────────┐ │ │
│ │ │ Arial (fallback) │ → │ Inter (web font) │ │ │
│ │ │ ▓▓▓▓▓▓▓▓▓▓▓▓ │ │ ▓▓▓▓▓▓▓▓▓▓ │ ← Narrower │ │
│ │ │ ▓▓▓▓▓▓▓▓ │ │ ▓▓▓▓▓▓ │ ← Taller │ │
│ │ └─────────────────────┘ └─────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │
│ WITH adjustFontFallback (Default): │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ 1. Adjusted fallback CSS generated at build time │ │
│ │ 2. Text renders with adjusted Arial (metrics match Inter) │ │
│ │ 3. Inter loads │ │
│ │ 4. Swap occurs - NO LAYOUT SHIFT │ │
│ │ │ │
│ │ Generated CSS: │ │
│ │ @font-face { │ │
│ │ font-family: '__Inter_Fallback'; │ │
│ │ src: local('Arial'); │ │
│ │ ascent-override: 90.49%; │ │
│ │ descent-override: 22.56%; │ │
│ │ line-gap-override: 0%; │ │
│ │ size-adjust: 107.06%; │ │
│ │ } │ │
│ │ │ │
│ │ ┌─────────────────────┐ ┌─────────────────────┐ │ │
│ │ │ Adjusted Arial │ = │ Inter (web font) │ ← Same size │ │
│ │ │ ▓▓▓▓▓▓▓▓▓▓ │ │ ▓▓▓▓▓▓▓▓▓▓ │ │ │
│ │ │ ▓▓▓▓▓▓ │ │ ▓▓▓▓▓▓ │ │ │
│ │ └─────────────────────┘ └─────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Manual Metric Override (Local Fonts)
import localFont from 'next/font/local'
const customFont = localFont({
src: './CustomFont.woff2',
display: 'swap',
// Manual metric adjustments
declarations: [
{ prop: 'ascent-override', value: '85%' },
{ prop: 'descent-override', value: '20%' },
{ prop: 'line-gap-override', value: '0%' },
{ prop: 'size-adjust', value: '105%' },
],
})
Common Pitfalls and Solutions
Pitfall 1: Missing Subset Warning
// ❌ Warning: No subset specified
const inter = Inter({})
// ✅ Always specify subset
const inter = Inter({
subsets: ['latin'],
})
Pitfall 2: Weight Required for Non-Variable Fonts
// ❌ Error: Weight is required for non-variable fonts
const roboto = Roboto({
subsets: ['latin'],
})
// ✅ Specify weight
const roboto = Roboto({
weight: ['400', '700'],
subsets: ['latin'],
})
Pitfall 3: Font Not Loading in Nested Layouts
// app/layout.tsx - Font defined here
const inter = Inter({ subsets: ['latin'] })
// app/blog/layout.tsx - Font NOT available here automatically
// Must import and apply again or use CSS variables
// ✅ Solution: Use CSS variables
// app/layout.tsx
const inter = Inter({
subsets: ['latin'],
variable: '--font-inter',
})
<html className={inter.variable}>
// app/blog/layout.tsx
// CSS variables inherited from parent
<div className="font-sans">{children}</div>
Pitfall 4: Multiple Instances of Same Font
// ❌ Creates duplicate font loads
// page1.tsx
const inter = Inter({ subsets: ['latin'] })
// page2.tsx
const inter = Inter({ subsets: ['latin'] })
// ✅ Define once, import everywhere
// lib/fonts.ts
export const inter = Inter({ subsets: ['latin'] })
// page1.tsx
import { inter } from '@/lib/fonts'
// page2.tsx
import { inter } from '@/lib/fonts'
Key Takeaways
- Use Variable Fonts: Single file for all weights, best performance
- Always Specify Subsets: Reduces font file size significantly
- Use display: 'swap': Best for body text, ensures content visibility
- Leverage adjustFontFallback: Automatic CLS prevention (enabled by default)
- Define Fonts Once: Create a font definitions file, import where needed
- CSS Variables for Flexibility: Easiest integration with Tailwind and custom CSS
- Preload Strategically: Global fonts in root layout, page-specific fonts where needed
- Self-Hosting is Automatic: No requests to Google from user browsers
- Use Font Definitions File: Centralize font configuration for maintainability
- Test Layout Shift: Use Chrome DevTools to verify CLS is zero
References
What did you think?