How to Use Self-Hosted Fonts with font-face in NextJS: Fix Compilation Stalls and Missing Fonts
In the world of web development, typography plays a pivotal role in user experience, brand identity, and readability. While third-party font services like Google Fonts are convenient, self-hosting fonts offers greater control over performance, privacy, and reliability—no more relying on external servers or dealing with GDPR compliance issues.
NextJS, a popular React framework, simplifies building fast, scalable web apps, but integrating self-hosted fonts can sometimes lead to frustrating issues: compilation stalls during development, missing fonts in production, or 404 errors for font files.
This guide will walk you through step-by-step how to properly self-host fonts in NextJS using @font-face, troubleshoot common pitfalls like compilation delays and missing fonts, and optimize your setup for performance. Whether you’re using the App Router (NextJS 13+) or the legacy Pages Router, we’ve got you covered.
Table of Contents#
- Prerequisites
- Setting Up Your NextJS Project
- Organizing Font Files in NextJS
- Using
@font-faceto Define Fonts - Integrating Fonts into NextJS (App Router & Pages Router)
- Common Issues & Fixes
- Best Practices for Performance
- Conclusion
- References
Prerequisites#
Before diving in, ensure you have:
- A NextJS project (v12+ recommended; we’ll cover both App Router and Pages Router).
- Node.js (v18+).
- Font files in modern formats: WOFF2 (preferred), WOFF, or TTF (as fallback). Avoid outdated formats like EOT.
- Basic knowledge of CSS and NextJS project structure.
Where to get fonts legally?
- Google Fonts (download files from fonts.google.com).
- Font Squirrel (fontsquirrel.com).
- Your own licensed fonts (ensure compliance with the font’s EULA).
Setting Up Your NextJS Project#
If you don’t have a NextJS project, create one with:
npx create-next-app@latest my-font-project
# Follow prompts: Choose App Router or Pages Router, TypeScript, ESLint, etc.
cd my-font-projectOrganizing Font Files in NextJS#
NextJS serves static assets (like fonts) from the public directory. This folder is accessible at the root of your app, so files here don’t require special imports—you can reference them directly via URLs like /fonts/[filename].
Recommended Structure#
Place fonts in a fonts subdirectory within public for clarity. Organize by font family, weight, and style:
public/
└── fonts/
└── inter/ # Font family name (e.g., "Inter")
├── Inter-Regular.woff2 # Normal weight (400), normal style
├── Inter-Bold.woff2 # Bold (700), normal style
├── Inter-Italic.woff2 # Normal weight (400), italic style
└── Inter-BoldItalic.woff2 # Bold (700), italic style
Why public?
- Files in
publicare served as static assets, so NextJS doesn’t process them during compilation (avoids webpack overhead). - Easy to reference via absolute URLs (e.g.,
/fonts/inter/Inter-Regular.woff2).
Using @font-face to Define Fonts#
The @font-face CSS at-rule lets you define custom fonts. You’ll need to declare it in a CSS file, specifying the font family name, file paths, weight, and style.
Basic @font-face Example#
Create a CSS file (e.g., global.css) and add:
/* global.css */
@font-face {
font-family: 'Inter'; /* Name to use in font-family declarations */
src: url('/fonts/inter/Inter-Regular.woff2') format('woff2'),
url('/fonts/inter/Inter-Regular.woff') format('woff'); /* Fallback */
font-weight: 400; /* Normal weight */
font-style: normal; /* Normal style */
font-display: swap; /* Prevents FOIT (Flash of Invisible Text) */
}
@font-face {
font-family: 'Inter'; /* Same family name for different weights/styles */
src: url('/fonts/inter/Inter-Bold.woff2') format('woff2'),
url('/fonts/inter/Inter-Bold.woff') format('woff');
font-weight: 700; /* Bold weight */
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Inter';
src: url('/fonts/inter/Inter-Italic.woff2') format('woff2'),
url('/fonts/inter/Inter-Italic.woff') format('woff');
font-weight: 400;
font-style: italic; /* Italic style */
font-display: swap;
}Key Properties Explained#
font-family: The name you’ll use to reference the font (e.g.,font-family: 'Inter').src: Path to the font file(s). Always listwoff2first (smallest, most efficient), followed by fallbacks.font-weight: Numeric value (100-900) or keywords (normal=400, bold=700).font-style:normal,italic, oroblique.font-display: swap: Ensures text is visible immediately with a fallback font while your custom font loads (avoids FOIT).
Integrating Fonts into NextJS (App Router & Pages Router)#
Now, inject the @font-face CSS into your NextJS app. The method depends on whether you’re using the App Router (NextJS 13+) or Pages Router.
Global Styles#
App Router (src/app/)#
- Create a global CSS file (e.g.,
src/app/globals.css) and paste your@font-facerules. - Import it into your root layout (
src/app/layout.tsx):
// src/app/layout.tsx
import './globals.css'; // Import global styles
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Self-Hosted Fonts in NextJS',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}Pages Router (src/pages/)#
- Create a global CSS file (e.g.,
src/styles/globals.css). - Import it into
_app.tsx(or_app.js):
// src/pages/_app.tsx
import '../styles/globals.css'; // Import global styles
import type { AppProps } from 'next/app';
export default function App({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />;
}CSS Modules#
If you prefer component-scoped styles, use CSS Modules.
- Create a module file (e.g.,
src/components/MyComponent.module.css):
/* MyComponent.module.css */
@font-face {
/* Same @font-face rules as above */
font-family: 'Inter';
src: url('/fonts/inter/Inter-Regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}
.title {
font-family: 'Inter', sans-serif;
font-weight: 700;
}- Use it in a component:
// src/components/MyComponent.tsx
import styles from './MyComponent.module.css';
export default function MyComponent() {
return <h1 className={styles.title}>Hello, Inter Font!</h1>;
}Note: Avoid defining @font-face in multiple modules (duplication). Use global styles for fonts used across components.
Tailwind CSS (Optional)#
If using Tailwind, map your custom font to a Tailwind class.
- In
tailwind.config.js, add the font totheme.fontFamily:
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
theme: {
extend: {
fontFamily: {
inter: ['Inter', 'sans-serif'], // 'Inter' matches font-family from @font-face
},
},
},
};- Use it in HTML:
<p className="font-inter font-bold">This uses Inter Bold via Tailwind!</p>Common Issues & Fixes#
Compilation Stalls#
Problem: NextJS development server (next dev) freezes or takes forever to compile when fonts are added.
Causes & Fixes:
- Large/Unoptimized Font Files: WOFF2 files should be small (ideally <100KB per weight). Use tools like Font Squirrel’s Webfont Generator to subset fonts (remove unused characters) or compress them.
- Incorrect File Paths: If fonts are not in
public, webpack may try to process them, causing delays. Always place fonts inpublic/fonts/for static serving. - Too Many Font Variants: Loading 10+ font files (e.g., all weights/styles) can slow builds. Only include what you need.
Missing Fonts (404 Errors)#
Problem: Fonts don’t load, and the browser console shows 404 Not Found for font files.
Causes & Fixes:
- Wrong File Path in
@font-face: Use absolute paths starting with/(e.g.,url('/fonts/inter/Inter-Regular.woff2')). Relative paths (e.g.,../fonts/...) fail becausepublicis the root. - Fonts Not in
public: If fonts are insrc/instead ofpublic, NextJS won’t serve them. Move them topublic/fonts/. - Typos in Filenames: Double-check filenames (e.g.,
Inter-Regular.woff2vs.inter-regular.woff2—case-sensitive on Linux/macOS).
Font Weight/Style Mismatches#
Problem: Text appears bold/italic when it shouldn’t, or vice versa.
Causes & Fixes:
- Mismatched
font-weightin@font-face: If you declarefont-weight: 700for a regular font file, text withfont-weight: 700will use the wrong file. Ensurefont-weightmatches the actual font file’s weight. - Missing
font-styleDeclarations: If you forget to definefont-style: italic, browsers may fake italics (ugly!). Always include@font-facerules for italic/bold variants.
Best Practices for Performance#
- Use WOFF2 Only: It’s 30% smaller than WOFF and supported by all modern browsers (Chrome, Firefox, Safari 12+).
- Subset Fonts: Remove unused characters (e.g., non-Latin scripts) with Fonttools or Glyphhanger.
- Preload Critical Fonts: For fonts used above the fold, preload them to reduce load time. Add this to your layout’s
<head>:
// In App Router (layout.tsx) or Pages Router (_document.tsx)
import Head from 'next/head';
// App Router example (layout.tsx)
export default function RootLayout({ children }) {
return (
<html lang="en">
<Head>
{/* Preload Inter Regular */}
<link
rel="preload"
href="/fonts/inter/Inter-Regular.woff2"
as="font"
type="font/woff2"
crossOrigin="anonymous"
/>
</Head>
<body>{children}</body>
</html>
);
}- Avoid FOIT with
font-display: swap: As shown earlier, this ensures text is visible while fonts load.
Conclusion#
Self-hosting fonts in NextJS gives you control over performance and privacy, but it requires careful setup. By organizing fonts in public/fonts/, using @font-face with correct paths and properties, and troubleshooting common issues like compilation stalls and 404s, you can ensure smooth, fast-loading typography.
Follow the best practices above to keep your app performant, and your users will thank you for the crisp, reliable fonts!