Type something to search...
How to Use next/font to Optimize Fonts in Next.js?

How to Use next/font to Optimize Fonts in Next.js?

Most Next.js projects start with fonts the old way: a link tag to fonts.googleapis.com in the root layout, a pair of preconnect hints, and a font-family rule in globals.css. It works, but it costs two extra origins on every first visit, sends every visitor's IP address to Google, and leaves the swap from fallback to web font to shift your layout however it likes. Next.js ships a built-in alternative, next/font, that fixes all three problems with a few lines of TypeScript, and most teams still only use a fraction of what it does.

This article covers next/font in the App Router from start to finish: loading Google Fonts and local font files, working with variable fonts and extra axes, exposing fonts as CSS variables for Tailwind CSS v4, organizing several fonts in one definitions file, controlling preloading and font-display, and verifying that the generated fallback actually prevents layout shift.

What next/font Does for You

next/font is a font loader built into Next.js. When you import a font from next/font/google or next/font/local, Next.js does the following at build time:

  1. Downloads and self-hosts the font files. Google Fonts are fetched during the build and served from your own domain as static assets. The browser never contacts Google.
  2. Generates the @font-face rules with a unique, scoped family name and font-display: swap by default.
  3. Preloads the font files on the routes that use them, using the subsets you specify.
  4. Creates a metric-matched fallback font with size-adjust, ascent-override, descent-override, and line-gap-override, so the swap from fallback to web font causes minimal layout shift.
  5. Serves files with long-lived cache headers and hashed filenames.
Problem with a plain Google Fonts linkWhat next/font does instead
Extra DNS, TCP, and TLS for two Google originsFiles served from your own domain
Visitor IP sent to GoogleNo browser requests to Google
Render-blocking third-party stylesheetCSS bundled with your app
Late font discoveryAutomatic preload on routes that use the font
Layout shift when the font swapsSize-adjusted fallback face generated for you

The privacy point is not academic. Loading Google Fonts from Google's CDN has been the subject of GDPR rulings in the EU, which is covered in are Google Fonts GDPR-compliant.

Loading a Google Font

The App Router setup goes in your root layout. Import the font as a function, call it at module scope, and apply its className or variable:

// app/layout.tsx
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";

const inter = Inter({
  subsets: ["latin"],
  display: "swap",
});

export const metadata: Metadata = {
  title: "Web Solution Master",
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" className={inter.className}>
      <body>{children}</body>
    </html>
  );
}

A few rules the font loader enforces:

  • Call it at module scope and assign it to a const. Calling a font function inside a component, or conditionally, fails at build time because Next.js analyzes these calls statically.
  • Font names with spaces use underscores. Roboto Mono becomes Roboto_Mono, and Source Serif 4 becomes Source_Serif_4.
  • Specify subsets. If preload is on, which is the default, Next.js warns when no subset is given, because it needs to know which file to preload.
  • The build needs network access for Google Fonts, since the files are downloaded at build time. Air-gapped CI environments should use next/font/local with committed files.

Variable Fonts and Static Weights

If the Google font is a variable font, you do not need to specify weights; you get the full weight range from one file:

import { Inter } from "next/font/google";

export const inter = Inter({
  subsets: ["latin"],
  display: "swap",
});

To include additional axes beyond weight, use the axes option. Only weight is included by default to keep the file small:

import { Fraunces } from "next/font/google";

export const fraunces = Fraunces({
  subsets: ["latin"],
  axes: ["opsz", "SOFT"],
  display: "swap",
});

For a non-variable font, you must list the weights and styles you need. Each one is a separate file:

import { Merriweather } from "next/font/google";

export const merriweather = Merriweather({
  subsets: ["latin"],
  weight: ["400", "700"],
  style: ["normal", "italic"],
  display: "swap",
});

Load only the weights you actually use. Four weights in two styles is eight files, and every one of them is a potential preload and a separate swap.

Loading Local Font Files

For licensed commercial fonts or files you have customized, use next/font/local. Paths are relative to the file that calls the loader:

// app/fonts.ts
import localFont from "next/font/local";

export const brand = localFont({
  src: [
    { path: "./fonts/BrandSans-Variable.woff2", weight: "100 900", style: "normal" },
    { path: "./fonts/BrandSans-Variable-Italic.woff2", weight: "100 900", style: "italic" },
  ],
  display: "swap",
  variable: "--font-brand",
  adjustFontFallback: "Arial",
  fallback: ["system-ui", "Arial", "sans-serif"],
});

Notes on the options:

  • weight: "100 900" declares a range for a variable file, which becomes the font-weight range in the generated @font-face.
  • adjustFontFallback for local fonts accepts "Arial" (the default), "Times New Roman", or false. Use "Times New Roman" for serif fonts so the fallback is reshaped from a similar design.
  • fallback adds families after the generated fallback in the font-family value.
  • declarations lets you add or override descriptors in the generated @font-face, for example [{ prop: "font-feature-settings", value: "'ss01' on" }].

Use WOFF2 files. If you only have TTF or OTF, convert and subset them first; the loader self-hosts whatever you give it, so a 400 KB TTF stays a 400 KB TTF.

Using CSS Variables and Tailwind CSS v4

The className approach applies one font to one element. For a design system with separate body, heading, and code fonts, CSS variables are more flexible. Set the variable option, and apply the .variable class names to html:

// app/fonts.ts
import { Inter, JetBrains_Mono } from "next/font/google";
import localFont from "next/font/local";

export const inter = Inter({
  subsets: ["latin"],
  display: "swap",
  variable: "--font-inter",
});

export const mono = JetBrains_Mono({
  subsets: ["latin"],
  display: "swap",
  variable: "--font-jetbrains-mono",
});

export const brand = localFont({
  src: "./fonts/BrandDisplay-Variable.woff2",
  weight: "500 800",
  display: "swap",
  variable: "--font-brand",
});
// app/layout.tsx
import { inter, mono, brand } from "./fonts";
import "./globals.css";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" className={`${inter.variable} ${mono.variable} ${brand.variable}`}>
      <body className="font-sans antialiased">{children}</body>
    </html>
  );
}

Each .variable class defines a CSS custom property holding the scoped font family plus its generated fallback. In Tailwind CSS v4, map those variables to theme tokens with @theme inline:

/* app/globals.css */
@import "tailwindcss";

@theme inline {
  --font-sans: var(--font-inter), system-ui, sans-serif;
  --font-mono: var(--font-jetbrains-mono), ui-monospace, monospace;
  --font-display: var(--font-brand), var(--font-inter), sans-serif;
}

The inline keyword matters. It tells Tailwind to put the variable reference directly into each generated utility instead of resolving it once on :root. Without it, the theme variable is resolved at the root, so the font breaks as soon as the next/font class sits on body or any other element below html. You now have font-sans, font-mono, and a new font-display utility. For a fuller Tailwind type setup, see how to configure typography in Tailwind CSS v4.

Without Tailwind, use the variables in plain CSS:

body {
  font-family: var(--font-inter), system-ui, sans-serif;
}

h1,
h2 {
  font-family: var(--font-brand), var(--font-inter), sans-serif;
}

code,
pre {
  font-family: var(--font-jetbrains-mono), ui-monospace, monospace;
}

Organizing Fonts in a Definitions File

Each call to a font function creates one hosted instance. If two components each call Inter(...), you get duplicate @font-face rules and possibly duplicate files. Define every font once in a single module, such as app/fonts.ts above, and import the objects wherever you need them.

A path alias makes the import clean everywhere:

{
  "compilerOptions": {
    "paths": {
      "@/fonts": ["./src/app/fonts"]
    }
  }
}
import { mono } from "@/fonts";

export function CodeBlock({ children }: { children: React.ReactNode }) {
  return <pre className={mono.className}>{children}</pre>;
}

Controlling Preloading

next/font preloads font files based on where the font object is used:

  • Used in a page: preloaded on that route only.
  • Used in a layout: preloaded on every route wrapped by that layout.
  • Used in the root layout: preloaded on every route.

That gives you a natural way to keep preloads lean. A font used only in the blog's code samples can be applied in app/blog/layout.tsx and will not be preloaded on the marketing pages.

You can also turn preloading off per font. Do this for fonts that are not needed for the first paint, such as an italic or a font used only below the fold:

export const serifItalic = Source_Serif_4({
  subsets: ["latin"],
  style: "italic",
  display: "swap",
  preload: false,
});

Preloading more than one or two files on a route tends to delay the LCP image and critical CSS, so be selective.

Choosing the display Value

display maps directly to font-display and defaults to "swap":

  • "swap" — text renders immediately in the adjusted fallback and swaps when the font loads. The right default for most sites, especially with the automatic fallback.
  • "optional" — the web font is used only if it is available almost immediately; otherwise the fallback is kept for that page view. Best when you want zero font-related shift and can accept that some first visits show the fallback.
  • "fallback" — a short block period and a limited swap window; a middle ground.
  • "block" — hides text while loading. Avoid it for body text; it can be acceptable for icon fonts.

Verifying the Fallback and Measuring the Result

Inspect what next/font generated:

  1. Run npm run build and npm run preview (or next start), then open the page.
  2. In DevTools, open Elements, select the html element, and look at the styles for the generated class. You will see the custom property containing a family such as 'Inter', 'Inter Fallback'.
  3. In the Sources or Network panel, open the generated CSS file and find the fallback @font-face. It uses src: local("Arial") with size-adjust and the override descriptors.
  4. In Network, filter by Font. Files should come from your own origin under /_next/static/media/, with no requests to fonts.gstatic.com.

To see the fallback on its own, right-click the font request in the Network panel, choose Block request URL, and reload. Line breaks should be nearly identical to the loaded page. If they are not, the fallback base may be wrong, for example Arial for a serif font. The underlying technique is explained in how to use size-adjust and font metric overrides to prevent CLS.

Then confirm with metrics: run Lighthouse with throttling and check that no layout shift culprits point to web fonts, and that the network dependency tree shows the font starting early thanks to the preload.

Migrating from a Google Fonts Link Tag

If your layout currently has something like this, it can all go:

<head>
  <link rel="preconnect" href="https://fonts.googleapis.com" />
  <link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="" />
  <link
    href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap"
    rel="stylesheet"
  />
</head>

Migration steps:

  1. Remove the link tags and preconnect hints from the layout.
  2. Create app/fonts.ts with the equivalent next/font/google calls and a variable for each family.
  3. Apply the .variable classes to html.
  4. Replace hard-coded font-family: "Inter" rules with var(--font-inter), or map the variables in Tailwind's @theme inline.
  5. Rebuild, then check the Network panel for any remaining requests to Google domains, which can come from third-party embeds.

If you are adding a font to a Next.js project for the first time and want the step-by-step basics, see how to add a custom font to the Next.js project.

Common Problems and Fixes

  • "Font loader values must be explicitly written literals." Options are analyzed at build time, so they cannot come from variables, imports, or function calls. Write them inline.
  • Build fails fetching Google Fonts. The build server has no internet access or a proxy blocks it. Allow access, or switch to next/font/local with files in the repository.
  • Font applies in development but not in production. Usually a class name or variable was applied to body while your CSS reads the variable at :root. Apply the variables on html, or use @theme inline in Tailwind.
  • Faux bold on headings. A static font was loaded without the heading weight. Add the weight to the weight array, or switch to the variable version.
  • Too many preloads. Fonts defined in the root layout preload on every route. Move route-specific fonts into nested layouts or set preload: false.

next/font FAQ

Not from the visitor's browser. Next.js downloads Google Fonts during the build and serves the files from your own domain. Only the build server contacts Google.

No. next/font serves files from your own origin, so there is no third-party connection to warm up, and it injects preload tags automatically on the routes where each font is used.

Yes. Set the variable option on each font, apply the variable class names to the html element, and map them to theme tokens with an inline theme block in your CSS. Utilities such as font-sans then use the next/font families and their generated fallbacks.

It generates a fallback font-face based on a local system font, adjusted with size-adjust and ascent, descent, and line gap overrides so it occupies nearly the same space as your web font. That keeps the swap from shifting the layout. It is enabled by default.

Swap is the default and works well because next/font also generates a metric-matched fallback. Choose optional if you want to eliminate font-related layout shift completely and can accept that some first-time visitors on slow connections see the fallback font.

Not directly. Images generated with ImageResponse need the raw font data, so you read the font file as an ArrayBuffer and pass it in the fonts option of ImageResponse. next/font handles fonts for rendered pages, not for generated images.

Conclusion

next/font is the most effective font optimization you can make in a Next.js project for the least effort. It self-hosts Google and local fonts, removes third-party requests and the privacy questions that come with them, preloads only where fonts are used, and generates a size-adjusted fallback that keeps the swap from moving your layout.

To get the full benefit, define every font once in a shared module, use variable fonts where possible, request only the subsets, weights, and axes you need, expose fonts as CSS variables for Tailwind's @theme inline, and keep preloads to the one or two files that matter for the first paint. Then verify the result in DevTools and Lighthouse so you know the fallback and preload are doing their jobs.

Here are some useful references for going deeper on next/font:

  1. Next.js Docs: Font Module API reference — every option for next/font/google and next/font/local.
  2. Next.js Docs: Getting Started: Fonts — the recommended App Router setup for Google and local fonts.
  3. Tailwind CSS: Theme variables — how @theme and @theme inline define font tokens and utilities.
  4. web.dev: Best practices for fonts — the loading principles that next/font automates.
  5. MDN Web Docs: size-adjust — the descriptor behind next/font's generated fallback faces.
Share :

Related Posts

Ascenders, Descenders, and Baselines: The Anatomy of a Letterform

Ascenders, Descenders, and Baselines: The Anatomy of a Letterform

You align an icon next to a button label and it looks a pixel or two too high, no matter how you adjust vertical-align. You set overflow: hidden

Continue Reading
Are Google Fonts GDPR-Compliant?

Are Google Fonts GDPR-Compliant?

In late 2022, thousands of small business owners in Germany and Austria opened letters demanding a few hundred euros in "damages" because their websi

Continue Reading
How to Audit Web Font Performance with Lighthouse?

How to Audit Web Font Performance with Lighthouse?

A client sends you a screenshot of their PageSpeed Insights report: performance score 61, LCP 3.9 seconds, and a vague list of warnings. They want to

Continue Reading