React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

next-fontvariable-fontssubsetsself-hostingweb-fonts

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/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.
  • The font files are served with 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.
  • When using the 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.
  • The subsets option 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 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.

Alternatives

AlternativeUse WhenDon't Use When
next/font/googleYou want self-hosted Google Fonts with zero configYou use a proprietary font not on Google Fonts
next/font/localYou 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 stackYou want maximum performance with zero font downloadsBrand guidelines require a specific typeface
Fontsource (npm packages)You want npm-managed font packages outside Next.jsnext/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-face declarations.
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?
  • 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.
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.style
How 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.