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-loaderlocated atpackages/next/src/build/webpack/loaders/next-font-loader/index.tsintercepts imports ofnext/font/local, reads the specified font files, and emits a virtual CSS module containing the@font-facedeclarations. This loader automatically handlesunicode-rangesubsetting andfont-displaysettings. -
Preload Manifest Creation: The
NextFontManifestPlugininpackages/next/src/build/webpack/plugins/next-font-manifest-plugin.tscollects 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 withfontLoaders: ['next/font/local', 'next/font/google'], ensuring the transformation works without Babel. Additionally,packages/next/src/cli/next-dev.tsmonitors for the deprecated@next/fontpackage 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
next/font/localinpackages/next/src/build/webpack/loaders/next-font-loader/index.tsgenerates zero-runtime@font-faceCSS from files in yourpublicfolder.- The
NextFontManifestPluginatpackages/next/src/build/webpack/plugins/next-font-manifest-plugin.tsautomatically creates preload links for optimal Core Web Vitals. - Define fonts with a
srcarray specifying paths, weights, and styles, and use thevariableproperty to expose CSS custom properties for Tailwind. - Apply the generated
classNameto your root layout's<html>tag to activate the font family and preloading. - Extend
tailwind.config.jswith the CSS variable to enable utility classes likefont-inter.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →