| | |

Next.js 13 ▲ Font Optimization — next/font & variable font loading

⏱️ Estimated read: 13 min | |

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

LoaderImportUse case
Googleimport { Inter } from 'next/font/google'Google Fonts
Localimport localFont from 'next/font/local'Custom font files

Key options

OptionRequiredPurpose
subsetsYes (when preload is true)Character subsets to preload
weightVariable fonts: no; non-variable: yesFont weights
styleNonormal, italic, or array
displayRecommendedswap for most cases
variableFor CSS variable integrationCustom property name
preloadNo (default true)Preload the font

Return properties

PropertyUsage
classNameApply to an element’s className
styleApply as inline style
variableApply to <html> or <body> for CSS variable

Variable vs non-variable fonts

TypeWeight optionFile count
VariableNot neededOne file for all weights
Non-variableRequiredOne 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

PitfallWhy It HappensFix
Font not appliedclassName not on an ancestor elementApply to <html> or <body> in root layout
Warning about subsetssubsets not specified with preload: trueAdd subsets: ['latin']
Duplicate font instancesLoader called in multiple filesCreate a fonts.ts utility and import from it
Non-variable font failsweight not specifiedAdd weight: '400' or an array
@next/font errorOld package still installedUninstall @next/font and use next/font
Layout shift still occursdisplay not set or font metrics differUse 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

ItemValue
Google loadernext/font/google
Local loadernext/font/local
Variable font weightNot specified
Non-variable font weightRequired
SubsetsRequired for preload (warning otherwise)
Displayswap recommended
CSS variable optionvariable: '--font-name'
Global applicationRoot layout <html>
Deprecated package@next/font (removed in Next.js 14)

Key takeaways:

  • next/font self-hosts fonts at build time. The browser requests the font from the application’s own domain, not from fonts.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 weight option is unnecessary. For non-variable fonts, weight is required, and each weight is a separate file .
  • The subsets option is not optional when preloading. The default preload is true, and without subsets Next.js cannot know which characters to preload, producing a warning. ['latin'] is the minimum for English-only applications .
  • The variable option 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/font package is obsolete. Since Next.js 13.2, next/font is built into the framework. Projects still using @next/font must 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.

Was this helpful?


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!