Web fonts are essential for beautiful typography, but they come with performance costs— external network requests, slow loading, and dreaded layout shift when fonts load. Next.js solves this with next/font, a powerful system that automatically optimizes fonts, self-hosts them (no external requests), and uses CSS size-adjust to eliminate layout shift completely. Let's master font optimization!
Why next/font?
❌ Traditional Web Fonts
<!-- Traditional approach -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
<!-- Problems: -->
<!-- ❌ External network request (slow) -->
<!-- ❌ Privacy concerns (Google tracks) -->
<!-- ❌ Layout shift when font loads -->
<!-- ❌ No optimization -->
<!-- ❌ Depends on external service -->✅ next/font
// Modern approach with next/font
import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'] });
// Benefits:
// ✅ Automatic optimization at build time
// ✅ Self-hosted (no external requests)
// ✅ Zero layout shift
// ✅ Privacy-friendly
// ✅ Smaller bundle size
// ✅ Better performancePerformance Benefits
- No external requests: Fonts self-hosted at build time
- Zero layout shift: CSS size-adjust prevents text reflow
- Optimized delivery: Only loads characters you use
- Automatic preloading: Critical fonts preloaded automatically
- Privacy-friendly: No tracking from external font services
Using Google Fonts
Basic Google Font Setup
import { Inter } from 'next/font/google';
// Configure font
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>
);
}
// ✅ Font applied to entire app
// ✅ Optimized automatically
// ✅ No layout shiftMultiple Font Weights
import { Inter } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
weight: ['400', '600', '700'], // Regular, Semibold, Bold
display: 'swap',
});
export default function RootLayout({ children }) {
return (
<html lang="en" className={inter.className}>
<body>{children}</body>
</html>
);
}
// Usage in CSS:
// font-weight: 400; → Regular
// font-weight: 600; → Semibold
// font-weight: 700; → Bold
// ✅ Loads only specified weights
// ✅ Smaller bundle than loading all weightsVariable Fonts (Recommended)
import { Inter } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
// Variable font - includes all weights in one file
// More efficient than multiple weight files
variable: '--font-inter', // Creates CSS variable
});
export default function RootLayout({ children }) {
return (
<html lang="en" className={inter.variable}>
<body>{children}</body>
</html>
);
}
// ✅ Variable font = all weights in one file
// ✅ More efficient than multiple files
// ✅ Smooth weight transitions
// ✅ Creates --font-inter CSS variableUsing Font CSS Variables
/* Font variable automatically available */
body {
font-family: var(--font-inter), system-ui, sans-serif;
}
.heading {
font-family: var(--font-inter);
font-weight: 700;
}
.body-text {
font-family: var(--font-inter);
font-weight: 400;
}
// ✅ Use CSS variable throughout styles
// ✅ Easy to change font later
// ✅ Works with CSS Modules and TailwindMultiple Google Fonts
import { Inter, Playfair_Display } from 'next/font/google';
// Sans-serif for body
const inter = Inter({
subsets: ['latin'],
variable: '--font-sans',
});
// Serif for headings
const playfair = Playfair_Display({
subsets: ['latin'],
variable: '--font-serif',
});
export default function RootLayout({ children }) {
return (
<html lang="en" className={`${inter.variable} ${playfair.variable}`}>
<body>{children}</body>
</html>
);
}
// Usage in CSS:
// body { font-family: var(--font-sans); }
// h1, h2, h3 { font-family: var(--font-serif); }
// ✅ Multiple fonts with CSS variables
// ✅ Each optimized separately
// ✅ Easy to use throughout appbody {
font-family: var(--font-sans), system-ui, sans-serif;
}
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-serif), Georgia, serif;
font-weight: 700;
}
.code {
font-family: 'Courier New', monospace;
}
// ✅ Sans for body text
// ✅ Serif for headings
// ✅ Clear typography hierarchyUsing Local Fonts
Loading Local Font Files
import localFont from 'next/font/local';
const myFont = localFont({
src: './fonts/MyFont.woff2',
display: 'swap',
variable: '--font-custom',
});
export default function RootLayout({ children }) {
return (
<html lang="en" className={myFont.variable}>
<body>{children}</body>
</html>
);
}
// ✅ Load custom fonts from your project
// ✅ Same optimization as Google Fonts
// ✅ Full control over font filesMultiple Font Files (Different Weights)
import localFont from 'next/font/local';
const myFont = localFont({
src: [
{
path: './fonts/MyFont-Regular.woff2',
weight: '400',
style: 'normal',
},
{
path: './fonts/MyFont-Italic.woff2',
weight: '400',
style: 'italic',
},
{
path: './fonts/MyFont-Bold.woff2',
weight: '700',
style: 'normal',
},
],
variable: '--font-custom',
});
export default function RootLayout({ children }) {
return (
<html lang="en" className={myFont.variable}>
<body>{children}</body>
</html>
);
}
// ✅ Multiple weight/style variants
// ✅ Browser picks correct file based on CSS
// ✅ Optimized loadingVariable Local Font
import localFont from 'next/font/local';
const geist = localFont({
src: './fonts/GeistVF.woff2',
variable: '--font-geist',
weight: '100 900', // Variable font weight range
});
export default function RootLayout({ children }) {
return (
<html lang="en" className={geist.variable}>
<body>{children}</body>
</html>
);
}
// ✅ Variable font with full weight range
// ✅ One file, all weights
// ✅ Smooth transitions between weightsIntegrating with Tailwind CSS
Configure Tailwind to Use next/font
import { Inter, JetBrains_Mono } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
variable: '--font-sans',
});
const jetbrainsMono = JetBrains_Mono({
subsets: ['latin'],
variable: '--font-mono',
});
export default function RootLayout({ children }) {
return (
<html lang="en" className={`${inter.variable} ${jetbrainsMono.variable}`}>
<body>{children}</body>
</html>
);
}import type { Config } from 'tailwindcss';
const config: Config = {
content: ['./app/**/*.{js,ts,jsx,tsx,mdx}'],
theme: {
extend: {
fontFamily: {
sans: ['var(--font-sans)', 'system-ui', 'sans-serif'],
mono: ['var(--font-mono)', 'Courier New', 'monospace'],
},
},
},
plugins: [],
};
export default config;
// ✅ Tailwind uses next/font variables
// ✅ Optimized fonts in utility classesUsing Fonts with Tailwind
export default function Home() {
return (
<div className="p-8">
{/* Uses Inter (--font-sans) */}
<h1 className="text-4xl font-bold mb-4">
Welcome to Next.js
</h1>
{/* Uses Inter */}
<p className="text-lg mb-6">
This text uses Inter font from next/font.
</p>
{/* Uses JetBrains Mono (--font-mono) */}
<code className="font-mono bg-gray-100 px-2 py-1 rounded">
const hello = "world";
</code>
</div>
);
}
// ✅ font-sans → Inter
// ✅ font-mono → JetBrains Mono
// ✅ Tailwind classes work seamlesslyFont Configuration Options
import { Inter } from 'next/font/google';
const inter = Inter({
// Required: Character sets to include
subsets: ['latin'],
// Can include: 'latin-ext', 'cyrillic', etc.
// Font weights to load
weight: ['400', '600', '700'],
// Or for variable fonts: weight: '100 900'
// Font styles
style: ['normal', 'italic'],
// Font display strategy
display: 'swap',
// Options: 'auto', 'block', 'swap', 'fallback', 'optional'
// Preload font (default: true for first page)
preload: true,
// CSS variable name
variable: '--font-inter',
// Fallback fonts
fallback: ['system-ui', 'arial'],
// Adjust spacing (advanced)
adjustFontFallback: true,
});
// ✅ Comprehensive configuration
// ✅ Control every aspect of font loadingDisplay Options Explained
// display: 'swap' (Recommended)
// Shows fallback immediately, swaps to web font when ready
// ✅ No invisible text
// ✅ Fast initial render
// ⚠️ Slight visual change when font loads
// display: 'optional'
// Only use web font if cached, else use fallback
// ✅ Best performance
// ✅ No layout shift
// ⚠️ Might not show web font on first visit
// display: 'block'
// Hides text until web font loads (up to 3s)
// ⚠️ Invisible text period (bad UX)
// display: 'fallback'
// Short block period, then swap
// Balance between block and swap
// display: 'auto'
// Browser decides strategy
// Recommendation: Use 'swap' for best balanceComplete Font Setup Examples
Example 1: Basic Blog Setup
import { Inter, Merriweather } from 'next/font/google';
import './globals.css';
// Sans-serif for UI
const inter = Inter({
subsets: ['latin'],
variable: '--font-sans',
display: 'swap',
});
// Serif for article content
const merriweather = Merriweather({
subsets: ['latin'],
weight: ['400', '700'],
variable: '--font-serif',
display: 'swap',
});
export default function RootLayout({ children }) {
return (
<html lang="en" className={`${inter.variable} ${merriweather.variable}`}>
<body className="font-sans">{children}</body>
</html>
);
}@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
body {
font-family: var(--font-sans), system-ui, sans-serif;
}
/* Article content uses serif */
article {
font-family: var(--font-serif), Georgia, serif;
line-height: 1.7;
}
/* Headings use sans */
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-sans), system-ui, sans-serif;
font-weight: 700;
}
}
// ✅ Sans for UI elements
// ✅ Serif for readable article content
// ✅ Clear typography hierarchyExample 2: Complete Design System
import { Inter, Fira_Code, Playfair_Display } from 'next/font/google';
// Primary sans-serif
const inter = Inter({
subsets: ['latin'],
variable: '--font-sans',
});
// Monospace for code
const firaCode = Fira_Code({
subsets: ['latin'],
variable: '--font-mono',
});
// Display font for headings
const playfair = Playfair_Display({
subsets: ['latin'],
weight: ['700'],
variable: '--font-display',
});
export default function RootLayout({ children }) {
return (
<html
lang="en"
className={`${inter.variable} ${firaCode.variable} ${playfair.variable}`}
>
<body>{children}</body>
</html>
);
}import type { Config } from 'tailwindcss';
const config: Config = {
content: ['./app/**/*.{js,ts,jsx,tsx,mdx}'],
theme: {
extend: {
fontFamily: {
sans: ['var(--font-sans)'],
mono: ['var(--font-mono)'],
display: ['var(--font-display)'],
},
},
},
};
export default config;export default function Home() {
return (
<div className="container mx-auto px-4 py-8">
{/* Display font for hero */}
<h1 className="font-display text-6xl mb-4">
Beautiful Typography
</h1>
{/* Sans for body */}
<p className="font-sans text-lg mb-6">
This is body text using Inter font.
</p>
{/* Mono for code */}
<pre className="font-mono bg-gray-100 p-4 rounded-lg">
<code>const greeting = "Hello, World!";</code>
</pre>
</div>
);
}
// ✅ font-display → Playfair Display
// ✅ font-sans → Inter
// ✅ font-mono → Fira Code
// ✅ Complete typography systemExample 3: Local Custom Fonts
import localFont from 'next/font/local';
const brandFont = localFont({
src: [
{
path: './fonts/BrandFont-Regular.woff2',
weight: '400',
style: 'normal',
},
{
path: './fonts/BrandFont-Bold.woff2',
weight: '700',
style: 'normal',
},
],
variable: '--font-brand',
});
const systemFont = localFont({
src: './fonts/SystemFont-VF.woff2',
variable: '--font-system',
weight: '100 900',
});
export default function RootLayout({ children }) {
return (
<html lang="en" className={`${brandFont.variable} ${systemFont.variable}`}>
<body>{children}</body>
</html>
);
}
// ✅ Custom brand fonts
// ✅ Full control over font files
// ✅ Same optimization as Google FontsFont Files Structure
Organization of font files and configuration
Select a file or folder to see details
Font Optimization Best Practices
1. Use Variable Fonts When Possible
// ✅ GOOD: Variable font (one file, all weights)
const inter = Inter({
subsets: ['latin'],
variable: '--font-inter',
});
// ❌ LESS OPTIMAL: Multiple weight files
const inter = Inter({
subsets: ['latin'],
weight: ['400', '500', '600', '700', '800'],
});
// Variable fonts are more efficient2. Preload Critical Fonts Only
// ✅ GOOD: Preload only critical fonts
const inter = Inter({
subsets: ['latin'],
preload: true, // Critical font used immediately
});
const displayFont = Playfair_Display({
subsets: ['latin'],
preload: false, // Not critical, load later
});
// Preload only fonts needed for initial render3. Limit Font Subsets
// ✅ GOOD: Only include needed character sets
const inter = Inter({
subsets: ['latin'], // English only
});
// ❌ BAD: Including unnecessary subsets
const inter = Inter({
subsets: ['latin', 'latin-ext', 'cyrillic', 'greek'],
// Loads characters you don't need
});
// Smaller bundle = faster loading4. Use CSS Variables
// ✅ GOOD: CSS variables for flexibility
const inter = Inter({
subsets: ['latin'],
variable: '--font-inter',
});
// Easy to reference in CSS, CSS Modules, Tailwind
// Changes in one place update everywhere
// ❌ LESS FLEXIBLE: Direct className
const inter = Inter({ subsets: ['latin'] });
<body className={inter.className}>
// Harder to override or customize5. Specify Font Display Strategy
// ✅ GOOD: Explicit display strategy
const inter = Inter({
subsets: ['latin'],
display: 'swap', // Best for most cases
});
// swap = show fallback immediately, swap when ready
// Prevents invisible text, minimal layout shift with size-adjustKey Takeaways
- next/font - automatic font optimization built into Next.js
- Zero layout shift - CSS size-adjust prevents text reflow
- Self-hosted - no external requests to Google Fonts
- Google Fonts - import from 'next/font/google'
- Local fonts - import from 'next/font/local'
- CSS variables - use 'variable' option for flexibility
- Variable fonts - one file, all weights (more efficient)
- Apply in root layout - fonts available app-wide
🎉 Styling Section Complete!
You've completed the Styling in Next.js section! You've mastered:
- ✅ CSS Modules for component-scoped styling
- ✅ Tailwind CSS setup and configuration
- ✅ Global styles and CSS variables
- ✅ Font optimization with next/font
You now have complete mastery of styling in Next.js! You can use CSS Modules for custom components, Tailwind for rapid development, CSS variables for theming, and next/font for optimized typography. These skills enable you to build beautiful, performant, and maintainable Next.js applications.
⚡ Complete Styling Stack
Combine all styling approaches: CSS Modules for complex components, Tailwind for layout and utilities, CSS variables for theming, and next/font for typography. Each excels at different tasks—use them together for the best results!