
How to Use next/font to Optimize Fonts in Next.js?
- Sajjad
- Typography
- 01 Oct, 2026
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:
- 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.
- Generates the
@font-facerules with a unique, scoped family name andfont-display: swapby default. - Preloads the font files on the routes that use them, using the subsets you specify.
- Creates a metric-matched fallback font with
size-adjust,ascent-override,descent-override, andline-gap-override, so the swap from fallback to web font causes minimal layout shift. - Serves files with long-lived cache headers and hashed filenames.
| Problem with a plain Google Fonts link | What next/font does instead |
|---|---|
| Extra DNS, TCP, and TLS for two Google origins | Files served from your own domain |
| Visitor IP sent to Google | No browser requests to Google |
| Render-blocking third-party stylesheet | CSS bundled with your app |
| Late font discovery | Automatic preload on routes that use the font |
| Layout shift when the font swaps | Size-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 MonobecomesRoboto_Mono, andSource Serif 4becomesSource_Serif_4. - Specify
subsets. Ifpreloadis 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/localwith 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 thefont-weightrange in the generated@font-face.adjustFontFallbackfor local fonts accepts"Arial"(the default),"Times New Roman", orfalse. Use"Times New Roman"for serif fonts so the fallback is reshaped from a similar design.fallbackadds families after the generated fallback in thefont-familyvalue.declarationslets 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:
- Run
npm run buildandnpm run preview(ornext start), then open the page. - In DevTools, open Elements, select the
htmlelement, and look at the styles for the generated class. You will see the custom property containing a family such as'Inter', 'Inter Fallback'. - In the Sources or Network panel, open the generated CSS file and find the fallback
@font-face. It usessrc: local("Arial")withsize-adjustand the override descriptors. - In Network, filter by Font. Files should come from your own origin under
/_next/static/media/, with no requests tofonts.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:
- Remove the
linktags andpreconnecthints from the layout. - Create
app/fonts.tswith the equivalentnext/font/googlecalls and avariablefor each family. - Apply the
.variableclasses tohtml. - Replace hard-coded
font-family: "Inter"rules withvar(--font-inter), or map the variables in Tailwind's@theme inline. - 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/localwith files in the repository. - Font applies in development but not in production. Usually a class name or variable was applied to
bodywhile your CSS reads the variable at:root. Apply the variables onhtml, or use@theme inlinein Tailwind. - Faux bold on headings. A static font was loaded without the heading weight. Add the weight to the
weightarray, 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:
- Next.js Docs: Font Module API reference — every option for next/font/google and next/font/local.
- Next.js Docs: Getting Started: Fonts — the recommended App Router setup for Google and local fonts.
- Tailwind CSS: Theme variables — how @theme and @theme inline define font tokens and utilities.
- web.dev: Best practices for fonts — the loading principles that next/font automates.
- MDN Web Docs: size-adjust — the descriptor behind next/font's generated fallback faces.


