Font Optimization
Self-host and optimize web fonts with next/font -- zero layout shift, no external network requests, automatic subsetting.
Search across all documentation pages
Self-host and optimize web fonts with next/font -- zero layout shift, no external network requests, automatic subsetting.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
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.
// 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:
display: "swap" for accessible font loadingnext/font downloads 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.Cache-Control: public, immutable, max-age=31536000 for optimal caching.next/font automatically generates @font-face declarations with the correct font-display, unicode-range, and src values.variable option, a CSS custom property (e.g., --font-sans) is set on the element where you apply the class. This integrates cleanly with Tailwind CSS fontFamily configuration.subsets option restricts which character ranges are included, reducing file size. For Latin-script languages, ["latin"] is usually sufficient.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
});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" },
],
});Font not applying -- Forgetting to add the className or variable class to a parent element means the font never activates. Fix: Apply inter.className (or inter.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 weight option 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 -- With font-display: optional, the browser may skip the custom font entirely if it does not load fast enough (within ~100ms). Fix: Use display: "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() or localFont() inside a "use client" file. Fix: Define fonts in a Server Component (like layout.tsx or a dedicated fonts.ts file) and apply the classes via the DOM.
| 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 |
@font-face declarations.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>.
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",
});className applies the font directly via a CSS class with a font-family rule.variable sets a CSS custom property (e.g., --font-sans) that you can reference in CSS or Tailwind config. Use variable when integrating with Tailwind.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.
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.
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.
"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.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>;
}import type { NextFont } from "next/dist/compiled/@next/font";
const inter: NextFont = Inter({ subsets: ["latin"] });
// Properties: inter.className, inter.variable, inter.styleEach 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" },
],
});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.
Reviewed by Chris St. John·Last updated Jul 19, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥