Back to CourseBeginner10 min read
Next JS /

Loading Fonts with next/font

Load Google Fonts and your own font files in Next.js with next/font: self-hosting, no layout shift, CSS variables, Tailwind font classes and a fonts file.

Bishal Biswas

Bishal Biswas

Self Learner

Published on: October 25, 2026
Beginner Topic

What you will learn

A good font gives a shop its personality. Book Nook could use a clean font for most text and a classic, bookish font for titles. The usual way to add a web font is a <link> tag to Google Fonts. That works, but it has three problems:

  • The visitor’s browser has to contact another company’s server (Google) before it can draw your text. That is slower, and it tells Google who visited your page.
  • The text first shows in a fallback font, then jumps when the real font arrives, because the two fonts have different sizes. This is the same layout shift you met with images.
  • You have to remember to load each font only once and only where it is needed.

The next/font module solves all three. It downloads the font files when you build your app and serves them from your own site. It also tweaks the fallback font so it takes up almost exactly the same space as the real one, so nothing jumps.

Think of it as buying the books for your shop in advance instead of sending a runner to the library every time a customer asks for one.

In this lesson you will:

  • understand the font code create-next-app already wrote for you
  • fix a small surprise in the starter CSS
  • add a second Google font for book titles and use it with Tailwind
  • load a font file of your own with next/font/local
  • keep all your fonts in one file

Step 1: read the starter font code

Open app/layout.js. This is exactly what create-next-app generated:

// app/layout.js
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
});

export const metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({ children }) {
  return (
    <html
      lang="en"
      className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
    >
      <body className="min-h-full flex flex-col">{children}</body>
    </html>
  );
}

Here is what each part does:

  • import { Geist, Geist_Mono } from "next/font/google": every Google font is a function you import by name. Spaces in a font name become underscores, so “Geist Mono” is Geist_Mono and “Playfair Display” would be Playfair_Display.
  • Geist({ ... }): calling the function sets the font up. It must be called at the top level of the file (outside any component) and stored in a const.
  • subsets: ["latin"]: a subset is a slice of the font with only the characters for some languages. latin covers English and most Western European languages. Smaller subsets mean smaller files.
  • variable: "--font-geist-sans": asks Next.js to create a CSS variable with this name that holds the font.
  • geistSans.variable is a class name. Putting it on <html> makes the --font-geist-sans variable available to the whole page.

Defining a CSS variable does not change any text yet. That happens in app/globals.css, which connects the variables to Tailwind (you saw this block in Styling with Tailwind CSS):

/* app/globals.css */
@theme inline {
  --color-background: var(--background);
  --color-foreground: var(--foreground);
  --font-sans: var(--font-geist-sans);
  --font-mono: var(--font-geist-mono);
}

Now the Tailwind class font-sans means “use Geist”, and font-mono means “use Geist Mono”.

Step 2: fix the Arial surprise

Scroll down in the same globals.css file and you will find this:

/* app/globals.css */
body {
  background: var(--background);
  color: var(--foreground);
  font-family: Arial, Helvetica, sans-serif;
}

The body is set to Arial. The starter app/page.js hides this, because its outer <div> has the font-sans class. But as soon as you write your own pages without that class, all your text is in Arial, not Geist.

Fix it by removing the font-family line and putting font-sans on the <body> instead:

/* app/globals.css */
body {
  background: var(--background);
  color: var(--foreground);
}
// app/layout.js
<body className="min-h-full flex flex-col font-sans">{children}</body>

Reload any page and inspect some text in DevTools. Under Computed, the font family is now something like Geist, "Geist Fallback". The names are generated by Next.js, and “Geist Fallback” is the size-adjusted fallback it made for you.

Step 3: add a font for book titles

Let us add Lora, a classic serif font, for headings. Lora is a variable font: one file contains every weight from regular to bold. Variable fonts are the best choice with next/font, because you do not need to list weights.

To keep things tidy, put all font setup in one file. Create app/fonts.js:

// app/fonts.js
import { Geist, Geist_Mono, Lora } from 'next/font/google';

export const geistSans = Geist({
  variable: '--font-geist-sans',
  subsets: ['latin'],
});

export const geistMono = Geist_Mono({
  variable: '--font-geist-mono',
  subsets: ['latin'],
});

export const lora = Lora({
  variable: '--font-lora',
  subsets: ['latin'],
});

Why a separate file? Every call to a font function creates its own copy of that font in your app. If two components both called Lora(...), the visitor could download it twice. Define each font once, export it, and import it wherever you need it.

Now update the layout to use the file and add the new variable to <html>:

// app/layout.js
import { geistSans, geistMono, lora } from './fonts';
import './globals.css';

export const metadata = {
  title: 'Book Nook',
  description: 'A small online bookshop',
};

export default function RootLayout({ children }) {
  return (
    <html
      lang="en"
      className={`${geistSans.variable} ${geistMono.variable} ${lora.variable} h-full antialiased`}
    >
      <body className="min-h-full flex flex-col font-sans">{children}</body>
    </html>
  );
}

Finally, tell Tailwind about it. Add one line to the @theme inline block:

/* app/globals.css */
@theme inline {
  --color-background: var(--background);
  --color-foreground: var(--foreground);
  --font-sans: var(--font-geist-sans);
  --font-mono: var(--font-geist-mono);
  --font-serif: var(--font-lora);
}

Tailwind already has a font-serif class; this line points it at Lora. Use it on titles:

// app/books/page.js
<h1 className="font-serif text-3xl font-bold">All books</h1>
// components/BookCard.js
<h2 className="mt-3 font-serif text-lg font-semibold">{book.title}</h2>

Because Lora is variable, font-bold and font-semibold just work.

Step 4: three ways to apply a font

The object a font function returns gives you three handles. You have used .variable. Here are all three:

// app/about/page.js
import { lora } from '../fonts';

export default function AboutPage() {
  return (
    <main className="p-8">
      {/* 1. className: applies the font directly to this element */}
      <h1 className={lora.className}>About Book Nook</h1>

      {/* 2. style: the same, as an inline style object */}
      <p style={lora.style}>We have sold books since 1998.</p>

      {/* 3. variable: used on html, then picked up by a Tailwind class */}
      <p className="font-serif">Come and visit us.</p>
    </main>
  );
}

With Tailwind, the variable way is the most flexible: you set the variable once on <html> and then pick fonts with classes anywhere.

Step 5: fonts that are not variable

Some fonts come in fixed weights only. For those, you must say which weight (or weights) you want:

// app/fonts.js
import { Lobster } from 'next/font/google';

export const lobster = Lobster({
  weight: '400',
  subsets: ['latin'],
  variable: '--font-lobster',
});

For several fixed weights, pass an array such as weight: ['400', '700']. Each weight is a separate file the visitor downloads, so only ask for the ones you use. If you are not sure whether a font is variable, look it up on Google Fonts, or just leave weight out: Next.js tells you if it is needed (see Common mistakes).

Step 6: your own font files

Maybe Book Nook has a special logo font that is not on Google Fonts. Put the file in your project, for example in app/fonts/, and load it with next/font/local:

app/
├── fonts/
│   └── BookNookDisplay.woff2
├── fonts.js
└── layout.js
// app/fonts.js
import localFont from 'next/font/local';

export const bookNookDisplay = localFont({
  src: './fonts/BookNookDisplay.woff2',
  variable: '--font-book-nook-display',
});

The src path is relative to the file that calls localFont, here app/fonts.js. Use the .woff2 format when you can; it is the smallest.

If the font comes as separate files per weight or style, pass an array:

// app/fonts.js
export const bookNookDisplay = localFont({
  src: [
    { path: './fonts/BookNookDisplay-Regular.woff2', weight: '400', style: 'normal' },
    { path: './fonts/BookNookDisplay-Bold.woff2', weight: '700', style: 'normal' },
  ],
  variable: '--font-book-nook-display',
});

Then add ${bookNookDisplay.variable} to the <html> classes and --font-display: var(--font-book-nook-display); to @theme inline in globals.css. Now font-display is a Tailwind class:

// components/Header.js
<Link href="/" className="font-display text-2xl">Book Nook</Link>

Notice the two different names. --font-book-nook-display is the variable that next/font creates, and --font-display is the Tailwind theme variable that points at it. Do not give them the same name: a variable that points at itself does not work.

Step 7: useful options

You rarely need more than subsets, variable and sometimes weight. A few others are good to know:

Option Default What it does
display 'swap' Show fallback text straight away, then swap in the font when it arrives.
preload true Add a preload link so the font starts downloading early.
fallback none Your own fallback fonts, for example ['Georgia', 'serif'].
adjustFontFallback on The size-adjusted fallback that prevents layout shift. Leave it on.
style 'normal' Ask for 'italic' as well, for example style: ['normal', 'italic'].

Where you load a font decides where it is preloaded. A font set up and used in the root layout is preloaded on every page. A font used only in one page file is preloaded only on that page. So if only the About page uses Lobster, use lobster.className only on that page, and visitors to other pages do not get that font file pushed at them.

Common mistakes

Calling the font function inside a component

./app/books/page.js:4:13
Error: Font loaders must be called and assigned to a const in the module scope

Next.js handles fonts while it builds, so it must be able to find every font call without running your components. Move the call to the top level of the file (or into app/fonts.js) and assign it to a const.

Forgetting the weight of a non-variable font

Error: Module not found: Can't resolve 'next/font/google/target.css'
Missing weight for Lobster. Available weights: 400

The first line looks scary, but the second line is the real message. Add weight: '400' (or one of the listed weights).

Text is still in Arial

You set up the font, but globals.css still has font-family: Arial, Helvetica, sans-serif; on body, and your element has no font-sans class. Remove the Arial line and put font-sans on <body>, as in Step 2.

Setting up the same font in many files

If Lora({ ... }) appears in three different components, Next.js treats it as three fonts. Define each font once in app/fonts.js and import it.

The font class is missing from html

If font-serif falls back to a plain serif font, check that ${lora.variable} is in the className of <html> in app/layout.js. The CSS variable only exists inside elements that have that class.

Quick recap

  • next/font downloads fonts at build time and serves them from your own site: faster, more private, and no layout shift.
  • Import Google fonts by name from next/font/google (spaces become underscores) and your own files with localFont from next/font/local.
  • Call font functions at the top level of a file and assign them to a const. Keep them all in app/fonts.js.
  • subsets: ['latin'] keeps files small. Non-variable fonts also need weight.
  • Use variable to create a CSS variable, put font.variable on <html>, and map it to a Tailwind class in @theme inline (--font-serif: var(--font-lora);).
  • Remove the starter font-family: Arial line and add font-sans to <body> so Geist is used everywhere.
  • Fonts used in the root layout are preloaded on every page; fonts used in one page only on that page.
script.js

// app/fonts.js: every font Book Nook uses, defined once and imported where needed
import { Geist, Geist_Mono, Lora } from 'next/font/google';
import localFont from 'next/font/local';
// Geist for body text (variable font, so no weight needed)
export const geistSans = Geist({
  variable: '--font-geist-sans', // used by font-sans in globals.css
  subsets: ['latin'],
});
// Geist Mono for code and prices
export const geistMono = Geist_Mono({
  variable: '--font-geist-mono', // used by font-mono
  subsets: ['latin'],
});
// Lora for book titles (variable font: font-semibold and font-bold just work)
export const lora = Lora({
  variable: '--font-lora', // used by font-serif
  subsets: ['latin'],
});
// Your own font file, path relative to this file
export const bookNookDisplay = localFont({
  src: './fonts/BookNookDisplay.woff2',
  variable: '--font-book-nook-display', // used by font-display
});
// --- app/layout.js: put every .variable on html, and font-sans on body
// <html lang="en" className={`${geistSans.variable} ${geistMono.variable} ${lora.variable} ${bookNookDisplay.variable} h-full antialiased`}>
//   <body className="min-h-full flex flex-col font-sans">{children}</body>
// </html>
// --- app/globals.css: connect the variables to Tailwind classes
// @theme inline {
//   --font-sans: var(--font-geist-sans);
//   --font-mono: var(--font-geist-mono);
//   --font-serif: var(--font-lora);
//   --font-display: var(--font-book-nook-display);
// }

Was this explanation helpful?

Your feedback helps us improve the content for everyone.