Font Optimization
Self-host and optimize web fonts with next/font -- zero layout shift, no external network requests, automatic subsetting.
Recipe
Quick-reference recipe card -- copy-paste ready.
// app/layout.tsx
import { Inter } from "next/font/google";
const inter = Inter({
subsets: ["latin"],
display: "swap",
});
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={inter.className}>
<body>{children}</body>
</html>
);
}// Variable font with CSS variable
const inter = Inter({
subsets: ["latin"],
variable: "--font-inter",
});
// Use in Tailwind CSS
<html className={inter.variable}>
// tailwind.config.ts: fontFamily: { sans: ["var(--font-inter)"] }When to reach for this: Every Next.js project that uses custom fonts. next/font eliminates FOUT (flash of unstyled text), self-hosts the font files, and requires zero external network requests.
Working Example
// app/layout.tsx -- multiple fonts with CSS variables
import { Inter, JetBrains_Mono } from "next/font/google";
const inter = Inter({
subsets: ["latin"],
variable: "--font-sans",
display: "swap",
});
const jetbrainsMono = JetBrains_Mono({
subsets: ["latin"],
variable: "--font-mono",
display: "swap",
});
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" className={`${inter.variable} ${jetbrainsMono.variable}`}>
<body className="font-sans antialiased">{children}</body>
</html>
);
}// tailwind.config.ts
import type { Config } from "tailwindcss";
const config: Config = {
theme: {
extend: {
fontFamily: {
sans: ["var(--font-sans)", "system-ui", "sans-serif"],
mono: ["var(--font-mono)", "Menlo", "monospace"],
},
},
},
};
export default config;// app/components/code-block.tsx
export function CodeBlock({ code }: { code: string }) {
return (
<pre className="font-mono bg-gray-900 text-green-400 p-4 rounded overflow-x-auto">
<code>{code}</code>
</pre>
);
}// Using a local custom font
// app/fonts.ts
import localFont from "next/font/local";
export const calSans = localFont({
src: [
{
path: "../public/fonts/CalSans-Regular.woff2",
weight: "400",
style: "normal",
},
{
path: "../public/fonts/CalSans-Bold.woff2",
weight: "700",
style: "normal",
},
],
variable: "--font-cal",
display: "swap",
});// app/layout.tsx -- combining Google and local fonts
import { Inter } from "next/font/google";
import { calSans } from "./fonts";
const inter = Inter({ subsets: ["latin"], variable: "--font-sans" });
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" className={`${inter.variable} ${calSans.variable}`}>
<body className="font-sans">
{children}
</body>
</html>
);
}What this demonstrates:
- Loading multiple Google Fonts with CSS variables
- Integrating font variables with Tailwind CSS
- Loading a local custom font with multiple weights
- Combining Google and local fonts in the same layout
display: "swap"for accessible font loading
Deep Dive
How It Works
next/fontdownloads font files at build time and self-hosts them alongside your static assets. No requests to Google Fonts (or any external CDN) are made by the browser at runtime.- The font files are served with
Cache-Control: public, immutable, max-age=31536000for optimal caching. next/fontautomatically generates@font-facedeclarations with the correctfont-display,unicode-range, andsrcvalues.- When using the
variableoption, a CSS custom property (e.g.,--font-sans) is set on the element where you apply the class. This integrates cleanly with Tailwind CSSfontFamilyconfiguration. - The
subsetsoption restricts which character ranges are included, reducing file size. For Latin-script languages,["latin"]is usually sufficient. - Variable fonts include all weights and styles in a single file, reducing the number of network requests compared to static font files.
Variations
Specific font weights (non-variable fonts):
import { Roboto } from "next/font/google";
const roboto = Roboto({
weight: ["400", "500", "700"],
subsets: ["latin"],
display: "swap",
});Font with multiple subsets:
const notoSans = Noto_Sans({
subsets: ["latin", "latin-ext", "cyrillic"],
display: "swap",
});Preloading a specific font:
// next/font automatically preloads fonts used in the root layout.
// For fonts used only in specific pages, import them in that page's layout.
// app/blog/layout.tsx
import { Merriweather } from "next/font/google";
const merriweather = Merriweather({
weight: ["400", "700"],
subsets: ["latin"],
variable: "--font-serif",
});
export default function BlogLayout({ children }: { children: React.ReactNode }) {
return <div className={merriweather.variable}>{children}</div>;
}Fallback font metrics (size-adjust):
// next/font automatically calculates size-adjust, ascent-override,
// descent-override, and line-gap-override for the fallback font to
// minimize CLS. No manual configuration needed.
const inter = Inter({
subsets: ["latin"],
display: "swap",
adjustFontFallback: true, // default -- generates matched fallback metrics
});TypeScript Notes
import type { NextFont } from "next/dist/compiled/@next/font";
// next/font functions return a NextFont object
const inter: NextFont = Inter({ subsets: ["latin"] });
// Key properties
inter.className; // string -- CSS class that applies the font
inter.variable; // string | undefined -- CSS variable class (if variable option used)
inter.style; // { fontFamily: string; fontWeight?: number; fontStyle?: string }
// Local font source type
import localFont from "next/font/local";
const myFont = localFont({
src: [
{ path: "./font-regular.woff2", weight: "400", style: "normal" },
{ path: "./font-bold.woff2", weight: "700", style: "normal" },
{ path: "./font-italic.woff2", weight: "400", style: "italic" },
],
});Gotchas
-
Font not applying -- Forgetting to add the
classNameorvariableclass to a parent element means the font never activates. Fix: Applyinter.className(orinter.variable) to<html>,<body>, or the relevant container. -
Variable fonts with explicit weights -- Some Google Fonts are available as variable fonts but you pass specific weights anyway, causing Next.js to download static font files instead. Fix: Omit the
weightoption for variable fonts to get the single variable font file. -
Multiple subsets increase bundle size -- Each subset adds to the font file size. Fix: Only include the subsets your content actually uses.
-
display: "optional"may cause invisible text -- Withfont-display: optional, the browser may skip the custom font entirely if it does not load fast enough (within ~100ms). Fix: Usedisplay: "swap"for most cases to guarantee the custom font eventually shows. -
Font flicker on client navigation -- If a font is loaded in a page-specific layout (not the root layout), it may flash during client-side navigation to that page. Fix: Load frequently used fonts in the root layout so they are always available.
-
next/font only works in Server Components -- You cannot call
Inter()orlocalFont()inside a"use client"file. Fix: Define fonts in a Server Component (likelayout.tsxor a dedicatedfonts.tsfile) and apply the classes via the DOM.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
next/font/google | You want self-hosted Google Fonts with zero config | You use a proprietary font not on Google Fonts |
next/font/local | You have custom font files (woff2, ttf, otf) | The font is available on Google Fonts |
Google Fonts CDN (<link>) | Never in Next.js -- next/font is strictly better | -- |
| System font stack | You want maximum performance with zero font downloads | Brand guidelines require a specific typeface |
| Fontsource (npm packages) | You want npm-managed font packages outside Next.js | next/font already handles self-hosting |
FAQs
What does next/font do and why should I use it?
- Downloads font files at build time and self-hosts them as static assets.
- Eliminates external requests to Google Fonts at runtime.
- Prevents layout shift (CLS) with automatic fallback font metric adjustment.
- Automatically generates
@font-facedeclarations.
How do I use a Google Font with Tailwind CSS?
Use the variable option to create a CSS custom property, then reference it in tailwind.config.ts:
const inter = Inter({ subsets: ["latin"], variable: "--font-sans" });
// tailwind.config.ts
fontFamily: { sans: ["var(--font-sans)", "system-ui"] }Apply inter.variable as a class on <html>.
How do I load a local custom font?
import localFont from "next/font/local";
const myFont = localFont({
src: [
{ path: "./font-regular.woff2", weight: "400" },
{ path: "./font-bold.woff2", weight: "700" },
],
variable: "--font-custom",
});What is the difference between className and variable on a font object?
classNameapplies the font directly via a CSS class with afont-familyrule.variablesets a CSS custom property (e.g.,--font-sans) that you can reference in CSS or Tailwind config. Usevariablewhen integrating with Tailwind.
Gotcha: My font is not applying even though I imported it. What is wrong?
You likely forgot to add inter.className or inter.variable as a class on a parent element (e.g., <html> or <body>). The font does not activate without the class being applied to the DOM.
Gotcha: I specified weights for a variable font and now my bundle is larger. Why?
Passing explicit weight values for a font that supports variable weights causes Next.js to download separate static font files instead of a single variable font file. Omit the weight option to get the single variable font.
Can I use next/font inside a Client Component?
No. next/font functions (Inter(), localFont()) can only be called in Server Components. Define fonts in layout.tsx or a dedicated fonts.ts file (without "use client") and apply the classes via the DOM.
What does display: "swap" versus display: "optional" do?
"swap": shows a fallback font immediately, then swaps to the custom font once loaded. Guarantees the custom font eventually appears."optional": the browser may skip the custom font entirely if it does not load within ~100ms. Can cause invisible or missing custom text.
How do I scope a font to a specific section of the app?
Import the font in a nested layout rather than the root layout:
// app/blog/layout.tsx
import { Merriweather } from "next/font/google";
const merriweather = Merriweather({ weight: ["400"], subsets: ["latin"], variable: "--font-serif" });
export default function BlogLayout({ children }) {
return <div className={merriweather.variable}>{children}</div>;
}What is the TypeScript type returned by next/font functions?
import type { NextFont } from "next/dist/compiled/@next/font";
const inter: NextFont = Inter({ subsets: ["latin"] });
// Properties: inter.className, inter.variable, inter.styleHow do I type the src array for localFont in TypeScript?
Each entry is an object with path (string), weight (string), and optionally style (string):
const myFont = localFont({
src: [
{ path: "./font-regular.woff2", weight: "400", style: "normal" },
{ path: "./font-italic.woff2", weight: "400", style: "italic" },
],
});Does next/font affect performance if I load many fonts?
Each subset and font file adds to the build output. Only include the subsets your content uses and limit the number of font families to what your design requires.
Related
- Image Optimization -- Optimizing images alongside fonts
- Server Components -- Where font configuration lives
- Static vs Dynamic -- Fonts are part of the static shell
- Composition Patterns -- Applying font classes across layouts