Suggest an editImprove this articleRefine the answer for “Font optimization (next/font) in Next.js”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**`next/font`** is Next.js's built-in font system that self-hosts fonts at build time, removes external requests, and eliminates layout shift via automatic `size-adjust`. ```tsx import { Inter } from 'next/font/google'; const inter = Inter({ subsets: ['latin'], display: 'swap', variable: '--font-inter' }); // Build output: WOFF2 self-hosted, @font-face inlined, <link rel="preload"> added ``` **Key:** `next/font` replaces `<link href="fonts.googleapis.com">` with zero-CLS self-hosted fonts.Shown above the full answer for quick recall.Answer (EN)Image**`next/font`** is Next.js's built-in font system that self-hosts fonts at build time, removes external network requests, and eliminates layout shift (CLS) through automatic `size-adjust`. ## Theory ### TL;DR - `next/font` downloads fonts at build time and serves them from your domain, no Google servers involved at runtime - Traditional `<link href="fonts.googleapis.com">` blocks rendering and causes CLS; `next/font` fixes both - `className` applies a font-family stack directly to an element; `variable` creates a CSS custom property like `--font-inter` for use in Tailwind or globals.css - `display: 'swap'` shows the fallback font immediately, then swaps when the custom font loads - Use `next/font` for any font in a production Next.js app; skip only for non-JS static sites where CLS is not a concern ### Quick example ```tsx // app/layout.tsx import { Inter } from 'next/font/google'; const inter = Inter({ subsets: ['latin'], display: 'swap', variable: '--font-inter', }); export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en" className={inter.variable}> <body className={inter.className}> {children} </body> </html> ); } // At build: WOFF2 downloaded, @font-face inlined in CSS, // <link rel="preload" as="font"> added to <head> automatically ``` `html` gets the CSS variable, `body` gets the font-family class. Both are needed when you want Tailwind integration alongside direct application. ### Traditional Google Fonts vs next/font The old approach adds `<link href="https://fonts.googleapis.com/css2?family=Inter">` to `<head>`. That tag fires a DNS lookup, downloads a CSS file, then downloads the font files. During those network trips the browser renders text in the fallback font at a different size. When the custom font finally arrives, the text jumps. That jump is CLS. `next/font` cuts all of that. Fonts land in your build output as static WOFF2 files. The `@font-face` rule is written into your CSS at build time. And `size-adjust` is computed from the font's actual vertical metrics (ascent, descent, line-gap ratios) so the fallback font matches the custom font's dimensions closely enough that no visible jump occurs. ### When to use - New Google Font needed: import from `next/font/google`, done, no other config required - Custom or paid font in WOFF2 format: use `next/font/local` with a path relative to the file where you define it - Multiple fonts (heading + body + mono): define each separately, apply CSS variables, connect them in your Tailwind config - Variable font with a weight range: add `axes: ['wght']` so a single WOFF2 file handles every weight - Already using `<link>` tags: migrate to `next/font` before going to production; CLS directly affects Core Web Vitals scores ### How it works at build time During `next build`, Next.js reads your font configs, fetches files from Google (or uses your local files), converts to WOFF2 if needed, and writes them into the build output. It generates an `@font-face` rule with a `size-adjust` value computed from the font's vertical metrics. That rule goes into global CSS. A `<link rel="preload" as="font">` tag is injected into `<head>` so the browser fetches the font early in the waterfall. At runtime there are zero external requests. Fonts are cached like any other static asset with long-lived cache headers. One thing worth knowing: in `next dev` this optimization is skipped and fonts load externally. You won't see the full benefit until `next build`. ### Key options reference | Option | What it does | Example | |---|---|---| | `subsets` | Character sets to include in the download | `['latin', 'cyrillic']` | | `weight` | Specific weights to load | `['400', '700']` | | `display` | CSS `font-display` value | `'swap'` (recommended) | | `variable` | Name for the CSS custom property | `'--font-inter'` | | `preload` | Whether to add `<link rel="preload">` | `true` (default) | | `adjustFontFallback` | Override the fallback font for `size-adjust` calc | `'Arial'` | | `axes` | Enable variable font axes | `['wght']` | ### Common mistakes **1. Wrong subset for non-Latin text** ```tsx // Wrong: only latin glyph data downloaded at build time const inter = Inter({ subsets: ['latin'] }); // <p>Привет</p> renders as squares or invisible text ``` Fix: `subsets: ['latin', 'cyrillic']`. The subset list defines what glyph data gets bundled. Missing glyphs cannot be synthesized at runtime. **2. `display: 'block'` on body text** ```tsx // Wrong: text invisible until font loads, up to 3 seconds const inter = Inter({ display: 'block' }); ``` `block` tells the browser to show nothing until the font arrives. For body text on a slow connection that means several seconds of blank content. Use `'swap'` (the default) for body fonts. Reserve `'block'` for icon fonts where showing a fallback character would be worse than showing nothing. **3. Applying `className` to `<html>` instead of `<body>`** ```tsx // Wrong: font-family on html doesn't cascade reliably to body <html className={inter.className}> <body>{children}</body> </html> ``` `className` (the font-family stack) goes on `<body>`. `variable` (the CSS custom property) goes on `<html>` so child elements can reference it. The correct split: ```tsx <html className={inter.variable}> <body className={inter.className}> ``` **4. Wrong path for local fonts** ```tsx // Wrong: path relative to project root doesn't work const font = localFont({ src: '../public/fonts/MyFont.woff2' }); ``` `next/font/local` resolves paths relative to the file where the font is defined, not the project root and not `public/`. Put WOFF2 files in `app/fonts/` next to your layout, then: ```tsx const font = localFont({ src: './fonts/MyFont-Regular.woff2' }); ``` **5. Missing `axes` for variable fonts** ```tsx // Wrong: only one static weight downloaded, variable axis ignored const jetbrains = JetBrains_Mono({ subsets: ['latin'], weight: '400' }); // fontVariationSettings at runtime won't interpolate other weights ``` Add `axes: ['wght']` to enable the full weight range from one WOFF2 file. Without it, Next.js downloads only the static weight you specified and the variable axis does nothing. ### Real-world usage - Vercel's own site uses Inter and SF Pro via `next/font` for zero CLS on marketing pages - Shadcn/ui scaffolding sets up Inter with a CSS variable for Tailwind theming out of the box - The create-t3-app template uses local fonts for admin dashboard layouts - Any Next.js project targeting 90+ Lighthouse performance scores needs CLS below 0.1; `next/font` is the direct path there ### Follow-up questions **Q:** What does `font-display: swap` actually do? **A:** It tells the browser to show the fallback font immediately and swap it for the custom font once loaded. The swap window is up to 3 seconds. Without it you'd get either invisible text (`block`) or the fallback font staying permanently (`fallback`/`optional`). **Q:** What is the difference between `className` and `variable`? **A:** `className` injects a `font-family` declaration directly onto the element. `variable` creates a CSS custom property like `--font-inter` that you reference in your CSS or Tailwind config. Use `variable` when fonts are managed through a design system; `className` works for quick one-off application. **Q:** How does `size-adjust` prevent layout shift? **A:** Next.js reads the font's vertical metrics (ascent ratio, descent ratio, line-gap) at build time and calculates a percentage to scale the fallback font so its text dimensions match the custom font. When the swap happens, element sizes don't change, so CLS stays at zero. **Q:** Does `next/font` work in the Pages Router? **A:** Yes. In the App Router you configure it in `app/layout.tsx`. In Pages Router you do it in `_app.tsx` or `_document.tsx`. App Router is preferred because it gives full `<head>` preload behavior, but both approaches work. **Q (senior):** A user reports text jumping on mobile Safari. You traced it to `next/font`. Walk through how you'd diagnose and fix it. **A:** First reproduce it: run Lighthouse in mobile mode in Chrome DevTools, check the CLS score and identify which elements shift. Then check whether it is a variable font with `axes` defined. iOS Safari below 15.4 has partial variable axis support, so weights set via `fontVariationSettings` may not interpolate correctly. Fix: specify `weight: ['100', '400', '700']` as a static array fallback. Also check `adjustFontFallback`: on iOS the default system fallback can be SF Pro rather than Arial, which has different metrics. Override it with `adjustFontFallback: 'Arial'` for a consistent `size-adjust` calculation across platforms. Junior answer is just "use swap"; the actual fix requires reproducing the issue, checking font axis support, and auditing fallback font metrics. ## Examples ### Google Fonts with Tailwind CSS integration A production layout with two Google Fonts wired into Tailwind via CSS variables: ```tsx // app/layout.tsx import { Inter, Roboto_Mono } from 'next/font/google'; const inter = Inter({ subsets: ['latin'], display: 'swap', variable: '--font-sans', }); const mono = Roboto_Mono({ subsets: ['latin'], weight: '400', display: 'swap', variable: '--font-mono', }); export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en" className={`${inter.variable} ${mono.variable}`}> <body className={inter.className}> {children} </body> </html> ); } ``` ```ts // tailwind.config.ts module.exports = { theme: { extend: { fontFamily: { sans: ['var(--font-sans)', 'system-ui'], mono: ['var(--font-mono)', 'monospace'], }, }, }, }; ``` Both fonts download at build time as subsets only. CSS variables flow into Tailwind's `font-sans` and `font-mono` utilities, so any component in the tree can use `className="font-sans"` or `className="font-mono"` without importing the font again. ### Local font with multiple weights When you have a custom branded font in WOFF2 format that is not on Google Fonts: ```tsx // app/layout.tsx import localFont from 'next/font/local'; const brand = localFont({ src: [ { path: './fonts/Brand-Regular.woff2', weight: '400', style: 'normal' }, { path: './fonts/Brand-Bold.woff2', weight: '700', style: 'normal' }, { path: './fonts/Brand-Italic.woff2', weight: '400', style: 'italic' }, ], display: 'swap', variable: '--font-brand', }); export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en" className={brand.variable}> <body className={brand.className}> {children} </body> </html> ); } ``` Font files sit in `app/fonts/` relative to `layout.tsx`. Next.js picks them up at build time, copies them into the output, and generates one `@font-face` rule per weight and style combination. No manual preload links needed. ### Variable font with weight axis One WOFF2 file can serve every weight if the font supports variable axes: ```tsx // app/layout.tsx import { JetBrains_Mono } from 'next/font/google'; const jetbrains = JetBrains_Mono({ subsets: ['latin'], axes: ['wght'], // enables the variable weight axis variable: '--font-code', display: 'swap', }); ``` ```tsx // components/CodeBlock.tsx export function CodeBlock({ children }: { children: React.ReactNode }) { return ( <pre className="font-code" style={{ fontVariationSettings: '"wght" 500' }} > {children} </pre> ); } ``` Without `axes: ['wght']`, Next.js downloads a single static weight and `fontVariationSettings` does nothing at runtime. With it, the one variable WOFF2 handles any weight in the font's supported range, and the bundle is smaller than loading three or four separate weight files.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.