How to Add Custom Local Fonts to a Next.js 13 Tailwind Project Using next/font

You can add custom local fonts to a Next.js 13 Tailwind project by configuring next/font/local with paths to your font files in the public folder, applying the generated className to your root layout, and optionally exposing a CSS variable via the variable property for Tailwind integration.

Next.js 13 introduced the built-in next/font API to eliminate external network requests and runtime JavaScript when loading typography. When you add custom local fonts to a Next.js 13 Tailwind project using next/font, the framework automatically generates optimized @font-face rules, injects preload hints, and exposes CSS variables for your utility classes. This implementation relies on a specialized webpack loader and manifest plugin within the vercel/next.js repository to deliver a zero-runtime solution.

How next/font/local Works Under the Hood

The local font system operates entirely at build time through three core mechanisms in the Next.js codebase:

  • Virtual CSS Generation: The next-font-loader located at packages/next/src/build/webpack/loaders/next-font-loader/index.ts intercepts imports of next/font/local, reads the specified font files, and emits a virtual CSS module containing the @font-face declarations. This loader automatically handles unicode-range subsetting and font-display settings.

  • Preload Manifest Creation: The NextFontManifestPlugin in packages/next/src/build/webpack/plugins/next-font-manifest-plugin.ts collects all generated font assets and constructs a manifest consumed by the server. During SSR, this manifest drives the automatic injection of <link rel="preload"> tags into the HTML <head>.

  • SWC Compiler Integration: As defined in packages/next/src/build/swc/options.ts, the SWC compiler (Next.js's default) is pre-configured with fontLoaders: ['next/font/local', 'next/font/google'], ensuring the transformation works without Babel. Additionally, packages/next/src/cli/next-dev.ts monitors for the deprecated @next/font package and emits migration warnings to ensure you're using the built-in API.

Step-by-Step Implementation

1. Place Font Files in the Public Directory

Store your font files (WOFF2 recommended for optimal compression) inside the public folder to ensure they are served as static assets:

/public/fonts/Inter/
├─ Inter-Regular.woff2
└─ Inter-Bold.woff2

2. Create the Font Configuration

Create a font definition file (commonly app/fonts.ts) that imports localFont from next/font/local and defines your font family:

// app/fonts.ts
import localFont from 'next/font/local';

export const inter = localFont({
  src: [
    {
      path: '/fonts/Inter/Inter-Regular.woff2',
      weight: '400',
      style: 'normal',
    },
    {
      path: '/fonts/Inter/Inter-Bold.woff2',
      weight: '700',
      style: 'normal',
    },
  ],
  variable: '--font-inter',
  display: 'swap',
});

The src array accepts objects with path, weight, and style properties, or string shortcuts for single-weight fonts. The variable property registers a CSS custom property (--font-inter) that Tailwind can reference.

3. Apply to the Root Layout

Import the font into your root layout and apply the generated className to the <html> element:

// app/layout.tsx
import './globals.css';
import { inter } from '@/app/fonts';

export const metadata = {
  title: 'Next.js Local Font Demo',
  description: 'Custom fonts with next/font',
};

export default function RootLayout({ 
  children 
}: { 
  children: React.ReactNode 
}) {
  return (
    <html lang="en" className={inter.className}>
      <body>{children}</body>
    </html>
  );
}

The inter.className value contains the generated font-family CSS and triggers the preload link injection during server-side rendering.

4. Configure Tailwind CSS

Extend your tailwind.config.js to map the CSS variable to a utility class:

// tailwind.config.js
module.exports = {
  content: [
    './app/**/*.{js,ts,jsx,tsx}',
    './pages/**/*.{js,ts,jsx,tsx}',
  ],
  theme: {
    extend: {
      fontFamily: {
        inter: ['var(--font-inter)'],
      },
    },
  },
  plugins: [],
};

Now you can apply the font using Tailwind utilities:

// app/page.tsx
export default function Home() {
  return (
    <h1 className="font-inter text-4xl font-bold">
      Hello from a local custom font!
    </h1>
  );
}

5. Verify the Generated Output

Run next build && next start, then inspect the server-rendered HTML. You should observe:

<link rel="preload" href="/_next/static/media/Inter-Regular.[hash].woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/_next/static/media/Inter-Bold.[hash].woff2" as="font" type="font/woff2" crossorigin>
<style data-next-font="local">
  @font-face {
    font-family: '__Inter_Fallback_123456';
    src: url(/_next/static/media/Inter-Regular.[hash].woff2) format('woff2');
    font-display: swap;
    /* additional @font-face rules */
  }
</style>

The fonts are now preloaded, cached, and served without any client-side JavaScript overhead.

Complete Working Example

Here is the full implementation combining all configuration files:

app/fonts.ts

import localFont from 'next/font/local';

export const inter = localFont({
  src: [
    {
      path: '/fonts/Inter/Inter-Regular.woff2',
      weight: '400',
      style: 'normal',
    },
    {
      path: '/fonts/Inter/Inter-Bold.woff2',
      weight: '700',
      style: 'normal',
    },
  ],
  variable: '--font-inter',
  display: 'swap',
});

app/layout.tsx

import './globals.css';
import { inter } from '@/app/fonts';

export const metadata = {
  title: 'Next.js + Tailwind Font Demo',
  description: 'Using next/font/local with Tailwind CSS',
};

export default function RootLayout({ 
  children 
}: { 
  children: React.ReactNode 
}) {
  return (
    <html lang="en" className={inter.className}>
      <body>{children}</body>
    </html>
  );
}

tailwind.config.js

module.exports = {
  content: [
    './app/**/*.{js,ts,jsx,tsx}',
    './pages/**/*.{js,ts,jsx,tsx}',
  ],
  theme: {
    extend: {
      fontFamily: {
        inter: ['var(--font-inter)'],
      },
    },
  },
  plugins: [],
};

Summary

Frequently Asked Questions

Can I use next/font/local with the Pages Router?

Yes. While the examples above use the App Router (app directory), next/font/local is fully compatible with the Pages Router. Import the font configuration in pages/_app.tsx and apply the className to your top-level component or a wrapper <div> instead of the <html> element.

What font formats are supported by next/font/local?

The loader accepts any standard web font format including WOFF2, WOFF, TTF, OTF, and EOT. However, WOFF2 is recommended for production due to superior compression and browser support. The next-font-loader passes the file references directly through to the generated @font-face rules without transformation.

Why does next/font use CSS variables for Tailwind integration?

The variable property creates a CSS custom property (e.g., --font-inter) that references the generated font-family name, which includes a hash to avoid collisions. This indirection allows Tailwind's JIT compiler to reference the font via fontFamily: ['var(--font-inter)'] without needing to know the hashed class name generated by the webpack loader at build time.

How does next/font improve performance compared to traditional @font-face?

Unlike manual @font-face declarations in global CSS, next/font eliminates flash of invisible text (FOIT) and flash of unstyled text (FOUT) by automatically setting font-display: swap. The NextFontManifestPlugin ensures fonts are preloaded via <link rel="preload"> tags, and because the CSS is generated at build time by the loader in packages/next/src/build/webpack/loaders/next-font-loader/index.ts, zero JavaScript is shipped to the client to handle font loading.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →