
Next.js Font Module
If you've already read a getting-started guide on fonts in Next.js, you know the basics: import a font from next/font/google, slap the className on your <html> tag, done. That's enough to get a font on the page, but it's nowhere near the full picture. The next/font module — covering both next/font/google and next/font/local — has eleven configuration keys, three different ways to apply the resulting styles, a whole system for avoiding duplicate font instances across a large app, and preloading behavior that depends on exactly which file you call the function from. Most of that never comes up in a five-minute tutorial, and most of it is exactly what you need once your app has more than one font, more than one layout, or more than one developer touching the font setup.
This is the reference-level tour: every option, what it actually does under the hood, where the two loaders diverge, and the mistakes that show up once a font setup grows past "one Google Font on the root layout."
Why This Module Exists in the First Place
Before Next.js shipped next/font, using a custom font in a React app meant one of two bad options: link to Google's CSS in your <head> and eat a render-blocking network request (plus send your visitors' IP addresses to Google on every page load), or manually download font files, self-host them, and hand-write @font-face rules yourself. Both approaches also tend to produce layout shift — the classic flash where text renders in a fallback font, then jumps and reflows the instant the real font finishes downloading.
next/font collapses all of that into a function call. At build time, it downloads the font files (Google or your own local files), generates optimized @font-face declarations with fallback metrics calculated to match the real font's dimensions, and serves everything from your own origin. No request to Google's servers ever happens in the browser. The CSS is inlined, the font files are static assets, and the fallback font is sized so precisely that the "swap" from fallback to real font causes close to zero layout shift. This is why the module is worth understanding at the option level — it isn't just a convenience wrapper, it's actively computing values you'd otherwise have to guess at.
The Full Option Reference
Both next/font/google and next/font/local are functions you call with a configuration object. Here's every key either one accepts, and which loader supports it:
| Option | Local | Type | Required? | |
|---|---|---|---|---|
src | No | Yes | string or array of objects | Yes (local only) |
weight | Yes | Yes | string or array | Required unless variable |
style | Yes | Yes | string or array | Optional |
subsets | Yes | No | array of strings | Optional |
axes | Yes | No | array of strings | Optional |
display | Yes | Yes | string | Optional |
preload | Yes | Yes | boolean | Optional |
fallback | Yes | Yes | array of strings | Optional |
adjustFontFallback | Yes | Yes | boolean or string | Optional |
variable | Yes | Yes | string | Optional |
declarations | No | Yes | array of objects | Optional |
Notice the asymmetry: src and declarations only make sense for local fonts (you're pointing at your own files), while subsets and axes only make sense for Google Fonts (you're selecting from Google's hosted catalog). Everything else — weight, style, display, preload, fallback, adjustFontFallback, variable — applies to both.
src
Only used with next/font/local. This is the path (or array of paths) to your font file, relative to wherever you're calling the function from.
// single file
const myFont = localFont({ src: "./fonts/my-font.woff2" });
// multiple files for one family — different weights/styles map to different files
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" },
],
});
The path resolution is relative to the file that calls the function, not the project root — a detail that trips people up the moment they move font-loading code into a shared module. If you call localFont() from app/page.tsx with src: '../styles/fonts/my-font.ttf', Next.js resolves that relative to app/page.tsx's location, landing on styles/fonts/my-font.ttf at the project root.
weight
For Google Fonts, this can be a single string ('400'), a string range for variable fonts ('100 900'), or an array of specific weights (['400', '700']) if the font isn't offered as a variable font. For local fonts, it's whatever weight label makes sense for your file — Next.js doesn't validate it against anything since it has no way of knowing what weights your file actually contains.
If the font is variable — meaning a single file smoothly interpolates across the whole weight range — you can skip weight entirely. This is the case for Inter, which is why every getting-started example never bothers setting it. The moment you reach for a fixed-weight (non-variable) Google Font, though, weight becomes required and the build will fail without it.
style
Accepts 'normal', 'italic', or (for local fonts only) any string value like 'oblique' since Next.js has no way to validate against your own files. Can be a single value or an array, same pattern as weight.
subsets
Google-only. This is the character subset — latin, latin-ext, cyrillic, and so on — restricting exactly which glyphs get downloaded. This matters more than it sounds: a font family with full Unicode coverage across every script can be enormous, and if your app is entirely in English, you have zero reason to ship Cyrillic or Devanagari glyphs to every visitor.
const inter = Inter({ subsets: ["latin"] });
Here's a detail the docs mention almost in passing but that will bite you in production: if preload is true (the default) and you don't specify subsets, Next.js emits a build warning, because it doesn't know which subset to actually preload a <link> tag for. Always set subsets explicitly rather than relying on defaults you never inspected.
axes
Also Google-only, and genuinely obscure. Variable fonts can expose more than just a weight axis — things like slnt (slant) on certain typefaces. By default, Next.js only includes the weight axis to keep bundle size down. If you need a non-standard axis, you look it up on Google's variable fonts page (filtering by axes other than wght) and pass it explicitly:
const inter = Inter({ axes: ["slnt"] });
Most projects never touch this option. If you're not intentionally using a font's extended variable axes, there's nothing to configure here.
display
Maps directly to the CSS font-display property: 'auto', 'block', 'swap', 'fallback', or 'optional'. The Next.js default is 'swap', which shows the fallback font immediately and swaps to the real font the moment it's ready — this is almost always what you want for text-heavy pages, since it avoids an invisible-text flash while the font downloads.
'optional' is worth knowing about for a specific case: if you want the font to essentially never cause layout shift, even at the cost of sometimes not showing your custom font at all (falling back permanently for that page load if the font doesn't arrive fast enough), optional is the value that prioritizes stability over always showing the "correct" typeface.
preload
A boolean, default true, controlling whether Next.js injects a <link rel="preload"> for the font. Preloading tells the browser to fetch the font file with high priority before it would otherwise discover the need for it — valuable for above-the-fold text, wasteful for a font used only deep in a rarely-visited page.
const decorative = Inter({ preload: false });
Set this to false for fonts used conditionally or far down the page — every preloaded font is competing for the browser's limited early-connection bandwidth with your actual critical resources.
fallback
An array of fallback font names, with no default, used while the real font loads (or if it fails to load at all):
const inter = Inter({ fallback: ["system-ui", "arial"] });
This works in tandem with adjustFontFallback below — Next.js doesn't just append these as generic CSS fallback fonts, it actively measures the target font's metrics and generates an adjusted fallback that matches its width and line-height as closely as possible.
adjustFontFallback
This is the option most responsible for next/font's layout-shift elimination, and it behaves differently between the two loaders, which is easy to miss:
next/font/google: a boolean, defaulttrue. Next.js automatically calculates a fallback font adjustment to minimize CLS.next/font/local: a string orfalse, default'Arial'. You can pass'Arial','Times New Roman', orfalseto disable the adjustment entirely.
The practical implication: for local fonts, if your custom font's proportions are wildly different from Arial (a very condensed or very wide display face, for instance), the automatic Arial-based adjustment can actually make the fallback-to-real-font swap more jarring, not less. In that specific case, setting adjustFontFallback: false and hand-tuning your own fallback stack via the fallback option can produce a smoother result than trusting the default.
variable
Declares a CSS custom property name so the font can be applied through CSS rather than only through the className prop:
const inter = Inter({ variable: "--font-inter" });
This is the option that unlocks everything from Tailwind integration to component-scoped styling, covered in its own section below.
declarations
Local-fonts-only, and rarely needed: an array of raw @font-face descriptor key-value pairs, for cases where you need to set something the other options don't expose:
const myFont = localFont({
src: "./my-font.woff2",
declarations: [{ prop: "ascent-override", value: "90%" }],
});
ascent-override, descent-override, and line-gap-override are the descriptors people reach for here — usually to manually fine-tune fallback font metrics beyond what adjustFontFallback computes automatically.
What You Get Back: The Returned Object
Calling either loader function returns an object with (at minimum) className and style, plus variable if you set that option. You're not limited to one way of consuming it:
// className — the common case
<p className={inter.className}>Hello, Next.js!</p>
// style — an object, useful when you need fontFamily programmatically
<p style={inter.style}>Hello World</p>
style.fontFamily specifically gives you the resolved font-family string (including the generated fallback chain) as a plain value, which is occasionally useful if you're composing styles dynamically rather than through static className strings.
The third option — CSS variables — is more involved and worth its own section, because it's the pattern that scales best once you have more than one font in an app.
CSS Variables: The Pattern That Actually Scales
className works fine for a single font applied once. The moment you have two or more fonts, or want font selection to live in your stylesheet instead of scattered across component files, CSS variables are the better tool:
// 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} antialiased`}
>
<body>{children}</body>
</html>
);
}
/* global.css */
html {
font-family: var(--font-inter);
}
h1 {
font-family: var(--font-roboto-mono);
}
Now the actual font assignment — which selector gets which typeface — lives entirely in CSS, where a designer or front-end developer can change it without touching the component that loads the font. The font-loading logic and the font-application logic are decoupled, which is the whole point.
Wiring This Into Tailwind
If you're on Tailwind CSS v4 (the version this very blog runs on), the integration is a one-line theme mapping:
/* global.css */
@import "tailwindcss";
@theme inline {
--font-sans: var(--font-inter);
--font-mono: var(--font-roboto-mono);
}
That's it — font-sans and font-mono utility classes now resolve to your next/font-loaded fonts, with all the self-hosting and layout-shift benefits intact underneath.
For Tailwind v3, the mapping happens in tailwind.config.js instead, under theme.extend.fontFamily:
// tailwind.config.js
module.exports = {
content: ["./app/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {
fontFamily: {
sans: ["var(--font-inter)"],
mono: ["var(--font-roboto-mono)"],
},
},
},
};
Either way, the underlying next/font setup is identical — only where you register the CSS variable with Tailwind changes.
Using Multiple Fonts Without Duplicating Instances
The docs are explicit about a subtlety that's easy to miss: every time you call Inter(...) or localFont(...), that's a separate font instance. If you accidentally call the same font's loader function in three different files with slightly different options, you get three separate hosted copies of that font shipped to the client — not one shared instance.
The fix is a font definitions file — load each font exactly once, in one place, and import the resolved object everywhere else you need it:
// styles/fonts.ts
import { Inter, Lora, Source_Sans_3 } from "next/font/google";
import localFont from "next/font/local";
export const inter = Inter();
export const lora = Lora();
export const sourceSansRegular = Source_Sans_3({ weight: "400" });
export const sourceSansBold = Source_Sans_3({ weight: "700" });
export const greatVibes = localFont({ src: "./GreatVibes-Regular.ttf" });
// app/page.tsx
import { inter, lora, sourceSansBold, greatVibes } from "@/styles/fonts";
export default function Page() {
return (
<div>
<p className={inter.className}>Hello world using Inter</p>
<p style={lora.style}>Hello world using Lora</p>
<p className={sourceSansBold.className}>Bold Source Sans 3</p>
<p className={greatVibes.className}>A decorative heading font</p>
</div>
);
}
A path alias (@/fonts mapped in tsconfig.json's paths) makes this even cleaner to import from anywhere in the app. This pattern is the single biggest lever for keeping a multi-font setup maintainable — treat it as the default approach the moment a second developer or a second font enters the picture, not something you retrofit after noticing duplicate downloads in the network tab.
The one-line piece of advice worth repeating from the official guidance here: use multiple fonts conservatively. Every additional font family is another resource the client has to fetch, parse, and render — even with all of next/font's optimizations, the cheapest font is the one you don't load.
Preloading Is Route-Scoped, Not Global
Here's a mental model correction that matters once your app has more than a couple of layouts: calling a font function does not make that font "globally available and preloaded everywhere." Preloading is scoped to where the function is actually referenced:
- Called inside a unique page file → preloaded only for that page's route.
- Called inside a layout → preloaded for every route nested under that layout.
- Called inside the root layout → preloaded across the entire app.
This means the common pattern of loading your primary font in the root layout and a decorative or secondary font only in the specific page/layout that uses it isn't just tidy code organization — it directly controls what gets preloaded where. Putting every font your app will ever use into the root layout, "just in case," silently makes every route pay the preload cost for fonts most of its visitors will never see rendered.
Version History Worth Knowing
If you're reading older tutorials or maintaining a project that predates the current API, two changes matter:
| Version | Change |
|---|---|
v13.2.0 | @next/font renamed to next/font; the separate package install is no longer required. |
v13.0.0 | @next/font was originally introduced. |
If you ever see import { Inter } from '@next/font/google' in a tutorial or a Stack Overflow answer, that's pre-13.2 syntax — on any current version, that import path from a separately installed package no longer exists, and you should be importing from next/font/google directly with zero extra installation.
Mistakes Worth Watching For
Forgetting the underscore convention. Multi-word Google Font names use underscores in the import, not spaces or hyphens: Roboto Mono is imported as Roboto_Mono, Source Sans 3 as Source_Sans_3. Get this wrong and you get a module-not-found error that doesn't obviously point at the naming rule.
Setting weight on a variable font "just to be safe." It's harmless in some cases but actively wrong in others — passing an array of specific weights to a font that's only available as a variable font will error, since the array-of-weights form is explicitly for non-variable Google Fonts.
Calling the same font's loader function in multiple files. Covered above, but worth repeating as its own mistake: this is the single most common way font setups quietly bloat, and it's invisible until someone checks the actual network payload.
Assuming adjustFontFallback's default is safe for every local font. The 'Arial' default assumption for local fonts works well for most body-text typefaces. It works poorly for display fonts, condensed fonts, or anything with proportions far from a standard sans-serif. If your fallback-to-loaded-font swap looks janky despite using next/font, this option — not display — is usually the fix.
Not specifying subsets and getting a silent-ish warning. The build warning is easy to miss in noisy CI logs. Set subsets explicitly on every Google Font, every time, as a habit rather than something you only add after noticing the warning.
Key Takeaways
| Question | Answer |
|---|---|
Do I need to install anything to use next/font? | No — since v13.2.0, it's built into Next.js itself, no separate package. |
| Google Fonts or local fonts — which options differ? | src and declarations are local-only; subsets and axes are Google-only; everything else applies to both. |
| How do I use the same font in two components without duplicating it? | A font definitions file — call the loader once, export the result, import everywhere. |
How do I apply a font through CSS instead of className? | Set the variable option, then reference var(--your-name) in your stylesheet or Tailwind theme. |
| Why does my fallback-to-real-font swap look jarring for a local display font? | Check adjustFontFallback — the 'Arial' default doesn't suit every typeface's proportions. |
| Does loading a font in one layout preload it everywhere? | No — preloading is scoped to the file the function is called from: page, layout, or root layout. |
next/font's real value isn't the one-liner import — it's that it's quietly doing font-metric math, subsetting, and self-hosting that would otherwise take a dedicated performance pass to get right by hand. Once you're past a single Google Font on a single layout, the options above are what determine whether your font setup stays fast and tidy or turns into an invisible source of duplicate downloads and layout shift nobody notices until a Lighthouse score drops.


