Next.js next/font Guide: Google Fonts, Local Fonts, and Zero Layout Shift (2026)
Self-host Google Fonts with next/font in Next.js 16 and hit zero layout shift. Covers local fonts, variable fonts, Tailwind integration, and the real gotchas.
The next/font module in Next.js 16 self-hosts Google Fonts and local font files at build time, which kills the render-blocking network request that used to cause layout shift. I've swapped Google Fonts CDN links for next/font across dozens of production apps, and the CLS improvement is usually immediate. Often 0.15 down to under 0.02 in a single deploy. This guide covers Google Fonts, local fonts, variable fonts, Tailwind CSS integration, size-adjust fallbacks, and the real gotchas you'll hit when shipping.
next/font/google downloads Google Fonts at build time and self-hosts them, so there's no runtime request to fonts.googleapis.com.
Both next/font/google and next/font/local auto-generate a size-adjusted fallback font, making Cumulative Layout Shift effectively zero.
Variable fonts (like Inter) are the way to go. One file covers all weights and produces smaller bundles than loading multiple static weights.
Use CSS variables (variable: '--font-inter') to expose fonts to Tailwind CSS via theme.fontFamily.
Font loaders must be called at module scope with string literals. Dynamic weights or variable names break the build-time optimization.
Preload only the fonts used above the fold. Set preload: false on secondary fonts to avoid burning render-blocking budget.
How does next/font work?
When you import a font through next/font/google, Next.js fetches the font files from Google's CDN at build time, embeds them in your .next/static output, and generates a CSS class that references them locally. At runtime, the browser never talks to fonts.googleapis.com, because the font is a same-origin asset served with immutable cache headers. This matters for three reasons: privacy (no third-party requests, which helps with GDPR compliance), performance (no DNS lookup, TLS handshake, or CSS round-trip before the font can load), and reliability (Google Fonts outages don't take your site down).
The second half of the story is CSS size-adjust. next/font measures each font's metrics (ascent, descent, cap height) and produces a fallback font declaration that visually matches the web font. While the real font is loading, the browser renders text using this synthetic fallback, and because the metrics match, no layout shift happens on the swap. It's the same technique web.dev's font-loading guidance recommends, but next/font does it automatically.
Setting up Google Fonts with next/font/google
For a global font applied across the app, import it in the root layout and attach its className to <html> or <body>. Here's the canonical setup for Inter, a variable font that ships as a single file:
Notice that the loader is called at module scope, not inside the component. This is a hard constraint. next/font uses a Babel/SWC plugin that inspects these calls statically, so anything dynamic (variable names, computed weights, ternary subsets) will fail with a pretty unhelpful error. If you need a font on only one route segment, import it in that segment's layout.tsx or page.tsx, and Next.js will still tree-shake correctly.
For fonts with multiple static weights (like Roboto), pass an array:
If you're using a licensed font that isn't on Google Fonts (think Sohne, GT America, or an in-house typeface), drop the files into your repo (I usually put them in app/fonts/) and load them through next/font/local. The loader accepts an array of src entries so you can map weights and styles to specific files:
Use only .woff2 in 2026. Every browser that runs Next.js 16 supports it, and .woff/.ttf fallbacks just bloat your bundle. If your foundry only shipped OTF, run them through a converter like Google's woff2 encoder before committing.
For local fonts, next/font can't auto-measure metrics from Google's database, so provide them explicitly with adjustFontFallback or use the fontpie CLI to generate the exact ascent/descent overrides. Skip this step and the fallback swap will cause visible shift, which I learned the hard way on a marketing site launch.
Variable fonts and multiple weights
Variable fonts store every weight, width, and style in a single file with interpolated axes. For a design system that uses six weights, a variable font is typically 40-60KB total; the equivalent set of static weights is 200-300KB. In Next.js, variable fonts need no special configuration. Just omit the weight option:
const inter = Inter({
subsets: ['latin'],
display: 'swap',
// No weight array - loads the full variable font
})
You then use any font-weight value in CSS (including non-standard values like 450 or 650) and the browser renders it correctly. This pairs nicely with a design token system where weights are semantic (--weight-body, --weight-emphasis) rather than numeric.
For advanced typographic control, variable fonts also expose custom axes like optical sizing (opsz) and grade (GRAD). Roboto Flex has 13 axes total. Check Google Fonts' variable font primer for the full list. Set them via CSS font-variation-settings once the font is loaded through next/font.
Does next/font work with Tailwind CSS?
Yes, and the integration is clean because next/font exposes fonts as CSS variables. Set the variable option on the loader, attach the variable class to <html>, then reference it in Tailwind's config:
Now className="font-sans" resolves to Inter, and font-mono resolves to JetBrains Mono. This is the pattern the official next/font documentation recommends, and it plays nicely with Tailwind's utility-first model. For Tailwind v4 with CSS-first configuration, use the @theme directive in your CSS instead:
The same principle applies when you use a design system like shadcn/ui. The components already reference font-sans, so wiring in your brand font takes one variable and no component edits. If you're building out a broader typography and layout system, our next/image optimization guide covers the visual-weight side of Core Web Vitals.
Why is my font causing layout shift?
If Chrome DevTools still shows a CLS spike after you switch to next/font, the culprit is almost always one of three things: the fallback metrics aren't matching, you're loading the font from CSS rather than through the loader, or a client-side useEffect is swapping the class after hydration. So, let's walk through each.
Fallback metrics mismatch. next/font uses size-adjust, ascent-override, and descent-override to make the fallback match the real font. For Google Fonts, these are computed from Google's font database. For local fonts, you must either provide them via adjustFontFallback or accept that the fallback will be approximate. Measure with the Chrome DevTools Performance panel, where layout shift events are highlighted in red on the timeline.
Loading fonts from CSS. If you still have @import url('https://fonts.googleapis.com/...') in your CSS or a <link> tag in your head, that path bypasses next/font entirely and reintroduces the render-blocking request. Grep your codebase for fonts.googleapis and fonts.gstatic and remove any hits. I hit this exact bug on a client project where an old @import had survived three refactors.
Client-side class swap. A common pattern I see is applying the font className inside a client component after mount, so the server-rendered HTML uses the fallback and the client swaps it. Always apply the font className on <html> or <body> in the root layout so it's present in the initial HTML payload.
Advanced options: subsets, display, preload
Three loader options have outsized impact on real-world performance:
subsets. Google Fonts are split into character subsets (latin, latin-ext, cyrillic, greek, etc.). Loading only what you need can cut font size by 60-80%. For an English-only site, subsets: ['latin'] is enough; if you support European languages with diacritics beyond basic Latin, add latin-ext. This isn't optional. next/font will throw a build error if you don't specify at least one subset.
display. Controls what happens while the font is loading. swap (the default in next/font) shows the fallback immediately and swaps when the web font is ready, which is the right choice for body text. optional shows the fallback and only swaps if the font loads within ~100ms, which is good for decorative fonts you can live without on slow connections. Avoid block, since it hides text entirely for up to 3 seconds.
preload. Defaults to true, which injects a <link rel="preload"> tag so the browser fetches the font in parallel with parsing. This is right for above-the-fold fonts. For a font used only in a footer or a rarely-visited page, set preload: false to avoid competing with your critical-path resources.
Common mistakes and how to fix them
Beyond the layout-shift causes above, five patterns break next/font in ways the error messages don't clearly explain:
Calling the loader inside a component body. The font is re-declared on every render and the SWC plugin can't optimize it. Move to module scope.
Passing dynamic values.Inter({ weight: someVariable }) breaks build-time analysis. All arguments must be literal.
Forgetting to attach the className. Importing the font without applying inter.className or inter.variable to an element loads the file but doesn't use it, adding weight for zero benefit.
Mixing className and variable.className applies the font directly; variable exposes it as a CSS custom property for downstream use. Pick one strategy per font: either apply the font directly, or route it through a CSS variable consumed by Tailwind or styled-components.
Bundling too many families. Every additional font adds bytes and blocks Largest Contentful Paint. Two families max (one for body, one for display) is the practical ceiling. For more ambitious typographic systems, use a single variable font with multiple axes.
For teams doing serious performance work, pair next/font with the techniques in our Next.js Bundle Analyzer guide. Fonts and JavaScript are the two biggest LCP levers, and they benefit from the same measurement discipline. If you're also chasing Core Web Vitals for images, the Next.js SEO guide covers how font choices feed into your Metadata API-driven Open Graph cards.
Frequently Asked Questions
Is next/font faster than loading Google Fonts from the CDN?
Yes. Because next/font self-hosts the font files, the browser saves a DNS lookup, TLS handshake, and CSS round-trip to fonts.googleapis.com, typically 100-300ms on cold connections. The font is also cached with immutable headers alongside your other static assets.
Can I use next/font in the Pages Router?
Yes. Import the loader in _app.tsx and apply the className to a wrapper div, since Pages Router doesn't own the <html> or <body> tags. All loader options work identically to the App Router.
Why does next/font require internet access at build time?
next/font/google downloads the actual font binaries from Google Fonts during next build. If your CI is offline or behind a strict firewall, use next/font/local with the files committed to your repo, or cache the fonts in a build artifact.
Does next/font work with server components?
Yes, and it works best in server components because the font className is embedded directly in the initial HTML, with no hydration flash. Client components can also apply the className, but the loader itself must be imported from a server-safe module.
How do I preload only specific fonts?
Set preload: false on secondary fonts, then manually add a preload link with <link rel="preload"> in your layout only on routes where the font appears above the fold. This keeps the render-blocking budget focused on critical fonts.
Enable Next.js Draft Mode in the App Router with draftMode(), signed __prerender_bypass cookies, and secure preview URLs for Sanity, Contentful, and DatoCMS.