Fonts (next/font)
Fonts can quietly wreck CLS. next/font self-hosts fonts, subsets to reduce size, and uses display strategies (swap, optional, block) to prevent layout shift and improve LCP.
Outcome
Fonts loaded via next/font with appropriate display strategy, fallback configuration, and variable fonts for flexibility. Self-hosted and subset for performance, CLS prevention with size-adjust.
Fast Track
- Replace Google Fonts
<link>withnext/font/googleimport. - Configure display strategy:
swap(show fallback, then custom),optional(skip if slow),block(wait briefly). - Use
adjustFontFallbackand variable fonts to reduce requests and prevent CLS.
Hands-On Exercise 3.5
The starter repo uses default system fonts. Your task is to add optimized custom fonts.
Requirements:
- Add
next/font/googleimports toapps/web/src/app/layout.tsx. - Use a variable font (e.g., Inter) to reduce requests and provide weight flexibility.
- Configure
display: 'swap'for immediate text visibility with fallback. - Enable
adjustFontFallbackfor size-matched fallback to prevent CLS. - Subset to
['latin']to reduce font file size. - Apply fonts via CSS variables for Tailwind integration.
Implementation hints:
- Display strategies:
swap: Show fallback immediately, swap when custom loads (recommended).optional: Skip custom font if network slow - use fallback only.block: Wait briefly for custom font, then swap (can delay text).fallback: Brief wait, then fallback, swap when ready.auto: Browser default behavior.
- Variable fonts provide all weights in single file - reduces requests.
- Subset to
['latin']or specific character sets to reduce file size. - Self-hosting automatic - no external requests to Google Fonts.
adjustFontFallbackuses size-adjust to match fallback metrics and prevent CLS.
The display strategy affects when users see text vs when custom fonts load. Use this prompt to pick the right one:
<context>
I'm implementing fonts with next/font and need to choose the correct `display` strategy.
The display strategy controls how text renders during font loading: show fallback immediately, wait for custom font, or skip custom font if slow.
My site type: [e.g., content site, e-commerce, dashboard, marketing landing page]
</context>
<current-implementation>
const inter = Inter({
subsets: ['latin'],
display: 'auto', // Current setting
})
</current-implementation>
<site-characteristics>
Describe your site's priorities:
- **Content type:** [News articles, product pages, documentation, marketing copy]
- **User priority:** [Fast initial text visibility vs brand-consistent typography]
- **Network conditions:** [Target users on fast connections, mobile 3G, or mixed]
- **Typography importance:** [Critical for brand identity vs functional only]
- **Performance goals:** [Optimize for LCP, CLS prevention, or font loading speed]
</site-characteristics>
<questions>
1. **Text visibility:** Is it more important to show text immediately (even in fallback font) or wait for custom font?
2. **CLS prevention:** Does font size mismatch between fallback and custom cause layout shift?
3. **Brand consistency:** Is showing fallback font briefly acceptable, or must users always see custom font?
4. **Slow networks:** For users on 3G, should we skip custom fonts entirely to prioritize text visibility?
5. **Multiple fonts:** Should body text and headings use different display strategies?
</questions>
<specific-scenario>
Example site: E-commerce product pages
- Priority: Fast text visibility for product descriptions and prices
- Network: Mixed (desktop and mobile users)
- Brand: Custom font nice-to-have but not critical
- CLS: Must prevent layout shift from font loading
Recommendation: display: 'swap' with adjustFontFallback: true
- Shows text immediately in fallback (fast LCP)
- Swaps to custom when loaded (brand consistency)
- Size-matched fallback prevents CLS
</specific-scenario>
For my site characteristics, recommend the optimal display strategy (swap, optional, block, fallback, auto) with rationale based on user experience priorities, network conditions, and performance trade-offs.This ensures your font loading strategy aligns with user experience goals and performance targets!
Try It
-
Verify self-hosting:
- Open Network tab, check no requests to fonts.googleapis.com or fonts.gstatic.com.
- Fonts served from your domain at
/_next/static/media/.
-
Test CLS prevention:
- Throttle network to "Slow 3G" in DevTools.
- Verify text renders with fallback, then swaps to custom font without layout shift.
- CLS should remain < 0.1.
-
Check font loading:
- Inspect
<style>tag in HTML with@font-facedeclarations. - Verify display strategy applied:
font-display: swap. - Check variable font includes multiple weights in single file.
- Inspect
Commit & Deploy
git add -A
git commit -m "feat(advanced): optimize fonts with next/font and fallback"
git push -u origin feat/advanced-font-optimizationDone-When
- DevTools Network tab: no requests to
fonts.googleapis.comorfonts.gstatic.com(fonts self-hosted) - Network tab: font files served from
/_next/static/media/with hash in filename - View page source: find
@font-facewithfont-display: swap(or your chosen strategy) - View page source: font file size reduced (subset to latin): look for WOFF2 files < 100KB
- Throttle to "Slow 3G", reload: text renders immediately in fallback font, then swaps to custom (no blank flash)
- Lighthouse Performance audit: CLS score < 0.1 (no layout shift from font swap)
Solution
Click to reveal solution
Update the layout to import and configure fonts:
import type { Metadata } from 'next'
import { Inter, JetBrains_Mono } from 'next/font/google'
import './globals.css'
// Variable font - all weights in single file
const inter = Inter({
subsets: ['latin'],
variable: '--font-inter',
display: 'swap',
adjustFontFallback: true,
})
// Monospace font for code blocks
const jetbrainsMono = JetBrains_Mono({
subsets: ['latin'],
variable: '--font-mono',
display: 'swap',
})
export const metadata: Metadata = {
title: process.env.NEXT_PUBLIC_APP_NAME || 'Vercel Academy Foundation - Web',
description: 'VAF Web',
}
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
return (
<html lang="en" className={`${inter.variable} ${jetbrainsMono.variable}`}>
<body className="container mx-auto px-4 py-8 font-sans">
{children}
</body>
</html>
)
}Update globals.css to use the font variables with Tailwind 4's @theme inline:
@import 'tailwindcss';
@import '@repo/ui/globals.css';
@theme inline {
--font-sans: var(--font-inter), ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--font-mono: var(--font-mono), ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
}Tailwind 4 uses @theme inline instead of @layer base for font configuration. The @layer base approach no longer works because Tailwind's utility classes (like font-sans) override base layer styles. By using @theme inline, you're overriding Tailwind's default --font-sans and --font-mono CSS variables, so the font-sans utility class automatically uses your custom fonts.
Local Fonts (Alternative)
If you need to use local font files instead of Google Fonts:
import localFont from 'next/font/local'
const customFont = localFont({
src: [
{
path: './fonts/custom-regular.woff2',
weight: '400',
style: 'normal',
},
{
path: './fonts/custom-bold.woff2',
weight: '700',
style: 'normal',
},
],
variable: '--font-custom',
display: 'swap',
adjustFontFallback: true,
})
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={customFont.variable}>
<body className="font-sans">
{children}
</body>
</html>
)
}Then override Tailwind's font variables in your CSS:
@import 'tailwindcss';
@theme inline {
--font-sans: var(--font-custom), ui-sans-serif, system-ui, sans-serif;
}swap (recommended): Shows fallback text immediately, swaps to custom font when loaded. Best for most use cases - ensures text visible quickly.
optional: Custom font optional - if network slow, skip it entirely and use fallback. Best for performance-critical pages.
block: Brief invisible period while custom font loads (up to 3s), then swap. Can delay text visibility - use sparingly.
fallback: Brief invisible period, then show fallback, swap when custom loads. Compromise between swap and block.
auto: Browser default - usually similar to block.
Use variable fonts instead of multiple static font files. Inter variable includes all weights (100-900) in a single file, reducing requests and improving performance compared to loading regular, medium, bold, etc. separately.
References
Was this helpful?