Next.js 13 ▲ Font Optimization — next/font & variable font loading
The next/font module is a build-time font loader that self-hosts font files and eliminates the external network request that a <link> tag to Google Fonts would make. It is built into Next.js since version 13.2; the separate @next/font package is deprecated and removed in Next.js 14 . The module works by downloading the font at build time, storing it as a static asset served from the same origin as the application, and generating the CSS to apply it without layout shift .
This chapter covers the two loaders that next/font provides: next/font/google for Google Fonts and next/font/local for custom font files. It explains the variable-font advantage, the subsets and display options that control loading behavior, the variable option that integrates with Tailwind CSS and other CSS solutions, and the component-scoping rule that determines where the font object should be instantiated.
Key point: Use variable fonts whenever possible. A variable font contains every weight and style in a single file, so you do not specify weight. For non-variable fonts, weight is required, and each weight is a separate file.
Why next/font exists
The external request problem. Loading Google Fonts with a <link> tag to fonts.googleapis.com creates a render-blocking request to a third-party domain. The browser must resolve DNS, establish a connection, and download the CSS before it can even begin downloading the font files. This adds latency and creates a privacy concern because Google receives the request. next/font downloads the font at build time and serves it from the application’s own domain, eliminating the runtime request entirely .
The layout shift problem. When a web font loads, the browser initially renders text with a fallback font, then swaps to the custom font. If the two fonts have different metrics, the text reflows and the layout shifts. next/font automatically calculates fallback font metrics and adjusts them to match the custom font, which reduces or eliminates the cumulative layout shift .
The scoping problem. A font loader function returns an object with className, style, and variable properties. Each call to a font loader creates a separate instance of that font. If the same font is loaded in multiple components, each call creates a duplicate instance. The documentation recommends loading the font once, typically in a utility file or the root layout, and importing the resulting object where needed .
a. Loading Google Fonts with next/font/google
The Google font loader is imported from next/font/google. The function name matches the font name, with underscores replacing spaces. For example, Roboto_Mono for Roboto Mono and Open_Sans for Open Sans .
A variable font does not require a weight option. The font file contains all weights, and the browser can interpolate between them.
// 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>
)
}
The subsets option is required when preload is true (the default). It tells Next.js which character subsets to preload. For English-only applications, ['latin'] is sufficient. For applications that support multiple scripts, additional subsets like ['latin', 'latin-ext', 'cyrillic'] can be specified, but each subset adds to the preloaded payload .
The display option controls the font loading behavior. swap shows the fallback text immediately and swaps to the custom font when it loads. This is the recommended value for most applications because it avoids invisible text .
For a non-variable font, the weight option is mandatory. It accepts a single value or an array of values. The style option accepts normal, italic, or an array of both.
// Non-variable font: weight is required
import { Roboto } from 'next/font/google'
const roboto = Roboto({
weight: ['400', '700'],
style: ['normal', 'italic'],
subsets: ['latin'],
display: 'swap',
})
b. Loading local fonts with next/font/local
The local font loader is imported from next/font/local. The src option points to the font file relative to the file that calls localFont. The font file can be placed in the public folder or co-located inside the app folder .
// app/layout.tsx
import localFont from 'next/font/local'
const myFont = localFont({
src: './my-font.woff2',
display: 'swap',
})
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={myFont.className}>
<body>{children}</body>
</html>
)
}
For a non-variable local font that has separate files for each weight, src can be an array of objects. Each object specifies the path, weight, and style. This tells Next.js which file corresponds to which weight and style, so the generated CSS includes the correct @font-face declarations.
const roboto = localFont({
src: [
{ path: './Roboto-Regular.woff2', weight: '400', style: 'normal' },
{ path: './Roboto-Italic.woff2', weight: '400', style: 'italic' },
{ path: './Roboto-Bold.woff2', weight: '700', style: 'normal' },
{ path: './Roboto-BoldItalic.woff2', weight: '700', style: 'italic' },
],
display: 'swap',
})
For a variable local font, a single file contains all weights. The src is a single path, and no weight specification is needed.
c. Multiple fonts and CSS variables
An application may need more than one font: a sans-serif for body text and a monospace for code, for example. The recommended approach is to use the variable option to define a CSS custom property for each font, apply the variables to the <html> element, and reference them in CSS.
// app/layout.tsx
import { Inter, Roboto_Mono } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
variable: '--font-inter',
display: 'swap',
})
const robotoMono = Roboto_Mono({
subsets: ['latin'],
variable: '--font-roboto-mono',
display: 'swap',
})
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={`${inter.variable} ${robotoMono.variable}`}>
<body>{children}</body>
</html>
)
}
The CSS file then references the variables:
/* app/globals.css */
body {
font-family: var(--font-inter);
}
code, pre {
font-family: var(--font-roboto-mono);
}
The variable option is what makes next/font compatible with Tailwind CSS. Tailwind’s font-sans and font-mono utilities can be mapped to the CSS variables in the Tailwind configuration or the @theme block, so the utility classes apply the loaded fonts .
Complete Example Session
// ============================================
// PART 1: VARIABLE GOOGLE FONT IN ROOT LAYOUT
// ============================================
// 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 font: no weight specified.
// The className applies the font to the whole app.
// ============================================
// PART 2: NON-VARIABLE FONT WITH WEIGHTS
// ============================================
import { Roboto } from 'next/font/google'
const roboto = Roboto({
weight: ['400', '700'],
style: ['normal', 'italic'],
subsets: ['latin'],
display: 'swap',
})
// Non-variable font: weight is required.
// Each weight is a separate file.
// ============================================
// PART 3: SUBSETS
// ============================================
const inter = Inter({
subsets: ['latin'],
})
// Only preload the Latin subset.
// Multiple subsets: ['latin', 'latin-ext', 'cyrillic']
// Each subset adds to the preloaded payload.
// ============================================
// PART 4: LOCAL FONT (SINGLE FILE)
// ============================================
import localFont from 'next/font/local'
const myFont = localFont({
src: './my-font.woff2',
display: 'swap',
})
// The path is relative to the file that calls localFont.
// ============================================
// PART 5: LOCAL FONT (MULTIPLE WEIGHTS)
// ============================================
const roboto = localFont({
src: [
{ path: './Roboto-Regular.woff2', weight: '400', style: 'normal' },
{ path: './Roboto-Bold.woff2', weight: '700', style: 'normal' },
],
display: 'swap',
})
// Each weight has its own file.
// Next.js generates the correct @font-face rules.
// ============================================
// PART 6: MULTIPLE FONTS WITH VARIABLES
// ============================================
import { Inter, Roboto_Mono } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
variable: '--font-inter',
display: 'swap',
})
const robotoMono = Roboto_Mono({
subsets: ['latin'],
variable: '--font-roboto-mono',
display: 'swap',
})
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={`${inter.variable} ${robotoMono.variable}`}>
<body>{children}</body>
</html>
)
}
/* ============================================
PART 7: REFERENCING VARIABLES IN CSS
============================================ */
body {
font-family: var(--font-inter);
}
code {
font-family: var(--font-roboto-mono);
}
// ============================================
// PART 8: TAILWIND INTEGRATION
// ============================================
// tailwind.config.js (Tailwind v3)
module.exports = {
theme: {
extend: {
fontFamily: {
sans: ['var(--font-inter)'],
mono: ['var(--font-roboto-mono)'],
},
},
},
}
// The font-sans and font-mono utilities now apply the loaded fonts.
// ============================================
// PART 9: SINGLE-PAGE FONT
// ============================================
// app/about/page.tsx
import { Montserrat } from 'next/font/google'
const montserrat = Montserrat({ subsets: ['latin'] })
export default function AboutPage() {
return (
<main className={montserrat.className}>
<h1>About</h1>
</main>
)
}
// Font is scoped to this page only.
// ============================================
// PART 10: FONT DEFINITIONS FILE
// ============================================
// app/fonts.ts
import { Inter, Roboto_Mono } from 'next/font/google'
export const inter = Inter({
subsets: ['latin'],
variable: '--font-inter',
})
export const robotoMono = Roboto_Mono({
subsets: ['latin'],
variable: '--font-roboto-mono',
})
// Import from here to avoid duplicate instances.
// Each font loader call creates a new instance.
The ten parts covered variable Google fonts, non-variable fonts with weights, subsets, local fonts with single and multiple files, multiple fonts with CSS variables, Tailwind integration, single-page fonts, and the font definitions file pattern.
Quick Reference
Loader imports
| Loader | Import | Use case |
|---|---|---|
import { Inter } from 'next/font/google' | Google Fonts | |
| Local | import localFont from 'next/font/local' | Custom font files |
Key options
| Option | Required | Purpose |
|---|---|---|
subsets | Yes (when preload is true) | Character subsets to preload |
weight | Variable fonts: no; non-variable: yes | Font weights |
style | No | normal, italic, or array |
display | Recommended | swap for most cases |
variable | For CSS variable integration | Custom property name |
preload | No (default true) | Preload the font |
Return properties
| Property | Usage |
|---|---|
className | Apply to an element’s className |
style | Apply as inline style |
variable | Apply to <html> or <body> for CSS variable |
Variable vs non-variable fonts
| Type | Weight option | File count |
|---|---|---|
| Variable | Not needed | One file for all weights |
| Non-variable | Required | One file per weight/style |
Best Practices
✅ Do This:
// Use variable fonts when possible
const inter = Inter({ subsets: ['latin'] }) # ✅
// Use the variable option for Tailwind integration
const inter = Inter({ subsets: ['latin'], variable: '--font-inter' }) # ✅
// Load the font once and import the object
// app/fonts.ts: export const inter = Inter(...) # ✅
// Apply the font in the root layout for global use
<html className={inter.className}> # ✅
❌ Don’t Do This:
// Use a <link> tag to Google Fonts
<link href="https://fonts.googleapis.com/css2?family=Inter" rel="stylesheet" /> # ❌
// Call the loader in every component
// Each call creates a new font instance # ❌
// Skip the subsets option
const inter = Inter({}) # ❌ (warning, no preload)
// Use @import in CSS for Google Fonts
@import url('https://fonts.googleapis.com/css2?family=Inter'); # ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Font not applied | className not on an ancestor element | Apply to <html> or <body> in root layout |
| Warning about subsets | subsets not specified with preload: true | Add subsets: ['latin'] |
| Duplicate font instances | Loader called in multiple files | Create a fonts.ts utility and import from it |
| Non-variable font fails | weight not specified | Add weight: '400' or an array |
@next/font error | Old package still installed | Uninstall @next/font and use next/font |
| Layout shift still occurs | display not set or font metrics differ | Use display: 'swap' and variable fonts |
Real-World Examples
1. Variable Google font
const inter = Inter({ subsets: ['latin'], display: 'swap' })
2. Non-variable Google font
const roboto = Roboto({ weight: ['400', '700'], subsets: ['latin'] })
3. Local font single file
const myFont = localFont({ src: './my-font.woff2' })
4. Local font multiple weights
const roboto = localFont({ src: [{ path: './Roboto-Regular.woff2', weight: '400' }] })
5. CSS variable
const inter = Inter({ subsets: ['latin'], variable: '--font-inter' })
6. Apply variables
<html className={`${inter.variable} ${robotoMono.variable}`}>
7. Tailwind config
fontFamily: { sans: ['var(--font-inter)'] }
8. Font definitions file
// app/fonts.ts
export const inter = Inter({ subsets: ['latin'], variable: '--font-inter' })
9. Single-page font
const montserrat = Montserrat({ subsets: ['latin'] })
<main className={montserrat.className}>
10. Remove old package
npx @next/codemod built-in-next-font .
Visual
The self-hosting pipeline
┌─────────────────────────────────────────────────────────────┐
│ NEXT/FONT PIPELINE │
│ │
│ Build time: │
│ Font loader called │
│ │ │
│ ▼ │
│ Font file downloaded (Google) or read (local) │
│ │ │
│ ▼ │
│ Stored as static asset │
│ │ │
│ ▼ │
│ CSS generated with @font-face and fallback metrics │
│ │ │
│ ▼ │
│ Runtime: │
│ Browser requests font from same origin │
│ No request to fonts.googleapis.com │
└─────────────────────────────────────────────────────────────┘
Variable vs non-variable
┌─────────────────────────────────────────────────────────────┐
│ VARIABLE FONT │
│ │
│ One file: Inter-Variable.woff2 │
│ Contains: weight 100–900, all styles │
│ Option: no weight needed │
│ │
│ NON-VARIABLE FONT │
│ │
│ Multiple files: │
│ Roboto-Regular.woff2 (weight 400) │
│ Roboto-Bold.woff2 (weight 700) │
│ Roboto-Italic.woff2 (weight 400 italic) │
│ Option: weight required │
└─────────────────────────────────────────────────────────────┘
CSS variable flow
┌─────────────────────────────────────────────────────────────┐
│ CSS VARIABLE FLOW │
│ │
│ const inter = Inter({ variable: '--font-inter' }) │
│ │ │
│ ▼ │
│ <html className={inter.variable}> │
│ │ │
│ ▼ │
│ CSS: --font-inter: 'Inter', 'Inter Fallback', sans-serif │
│ │ │
│ ▼ │
│ body { font-family: var(--font-inter); } │
│ │
│ Tailwind: fontFamily: { sans: ['var(--font-inter)'] } │
└─────────────────────────────────────────────────────────────┘
Component scoping
┌─────────────────────────────────────────────────────────────┐
│ WHERE TO LOAD THE FONT │
│ │
│ Global font: │
│ app/layout.tsx │
│ <html className={inter.className}> │
│ │
│ Single page: │
│ app/about/page.tsx │
│ <main className={montserrat.className}> │
│ │
│ Shared across components: │
│ app/fonts.ts (export the font object) │
│ Import where needed │
│ │
│ Load once. Each loader call creates a new instance. │
└─────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Google loader | next/font/google |
| Local loader | next/font/local |
| Variable font weight | Not specified |
| Non-variable font weight | Required |
| Subsets | Required for preload (warning otherwise) |
| Display | swap recommended |
| CSS variable option | variable: '--font-name' |
| Global application | Root layout <html> |
| Deprecated package | @next/font (removed in Next.js 14) |
Key takeaways:
next/fontself-hosts fonts at build time. The browser requests the font from the application’s own domain, not fromfonts.googleapis.com. This eliminates the third-party request, improves privacy, and removes a render-blocking round trip .- Variable fonts are the recommended choice. They contain all weights in a single file, so the
weightoption is unnecessary. For non-variable fonts,weightis required, and each weight is a separate file . - The
subsetsoption is not optional when preloading. The defaultpreloadis true, and withoutsubsetsNext.js cannot know which characters to preload, producing a warning.['latin']is the minimum for English-only applications . - The
variableoption is the integration point for CSS and Tailwind. It generates a CSS custom property that can be referenced in any stylesheet or mapped to Tailwind’s font utilities. This is how the font loaded in the root layout becomes available to every component . - Fonts are scoped to where they are loaded. A font loaded in the root layout applies globally. A font loaded in a page applies only to that page. The font object should be loaded once and imported where needed to avoid duplicate instances .
- The
@next/fontpackage is obsolete. Since Next.js 13.2,next/fontis built into the framework. Projects still using@next/fontmust migrate with the codemod and uninstall the package .
Remember: Font optimization in Next.js is about removing the external request and eliminating layout shift. next/font does both by self-hosting the font at build time and generating adjusted fallback metrics. Use variable fonts, specify the subsets you need, set display: 'swap', and use the variable option if the font needs to integrate with Tailwind or a global CSS strategy. Load the font once in the root layout for global application, or in a specific page for scoped use. And do not use <link> tags or @import for Google Fonts; the entire point of next/font is to eliminate those requests.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!