Tailwind Font Family: One next/font Import, 46,988 Bytes, Zero Google Requests
Tailwind's font-family utilities point at a CSS variable next/font has to define. One Outfit import, two self-hosted .woff2 files, 46,988 bytes, zero requests to Google's font servers.
Tailwind's font-family utilities (font-sans, font-serif, font-mono) apply whatever font stack you've named in a --font-* CSS variable — Tailwind itself ships only generic fallback stacks. The actual font has to come from somewhere else: a <link> to Google Fonts, a self-hosted @font-face, or next/font, which is what this site uses. One Outfit import, two generated .woff2 files totaling under 46 KB, and zero requests ever sent to Google's font servers.
Where this codebase's font-sans actually points
Tailwind v4 doesn't touch fonts directly — it generates a utility class for whatever value a token holds:
/* src/app/globals.css */
@theme {
--font-sans: var(--font-outfit), ui-sans-serif, system-ui, sans-serif;
}
font-sans in a className now compiles to that variable chain, and --font-outfit doesn't exist until something defines it. That something is next/font, called once in the root layout:
// src/app/layout.tsx
import { Outfit } from "next/font/google";
const outfit = Outfit({
variable: "--font-outfit",
subsets: ["latin"],
display: "swap",
});
<body className={`${outfit.variable} font-sans antialiased`}>
outfit.variable is a generated class (__variable_xxxxxx) that sets --font-outfit to the actual font-family value next/font resolved — a locally-hosted file, never a Google URL. font-sans then reads that variable through the @theme chain above. Three files, one string (--font-outfit), and the whole site's typography routes through it: no component imports a font directly, and no CSS anywhere else names Outfit by string. The weight axis is centralised the same way — font-weight in Tailwind counts which of the nine named steps this codebase actually uses, and which two atoms set them.
What next/font actually ships
next build downloads Outfit at build time, subsets it, and writes the result into .next/static/media as static assets served from this domain:
1b99372b3eaef0c8-s.p.1gsd1jahc5dg_.woff2 32,228 bytes
b2ea385cb5ae8625-s.1spbknb88wd48.woff2 14,760 bytes
total 46,988 bytes (~45.9 KiB)
Two files, not one, because next/font splits by what the browser needs immediately versus what it can fetch lazily — the .p. file is preloaded (referenced by a <link rel="preload"> the framework injects), the other loads on demand. Both are .woff2, the smallest format every supported browser accepts. No tailwind.config.js font block, no @font-face written by hand, and — because subsets: ["latin"] is explicit — no Cyrillic, Greek or CJK glyph data included for a site that never renders any.
Compare that to the alternative every "how do I add a custom font in Tailwind" tutorial reaches for first: a <link href="https://fonts.googleapis.com/..."> tag. That approach makes the browser open a connection to Google's servers before it can render text at all — a render-blocking request this site's Lighthouse-100 pages never pay, because next/font inlines the @font-face declarations at build time and serves the files from the same origin as everything else.
display: "swap", and what happens without it
The third option in the Outfit() call controls what the browser shows while the font file is still downloading:
font-display value | Before the font loads | After it loads |
|---|---|---|
swap (this site) | Fallback font (ui-sans-serif, system-ui, sans-serif) renders immediately | Swaps to Outfit once downloaded — a visible but brief reflow |
block (framework default if unset) | Text is invisible for up to ~3 seconds (FOIT) | Renders once the font arrives, or falls back after the timeout |
optional | Fallback renders; browser may skip the swap entirely on a slow connection | Only swaps if the font was already cached or arrives near-instantly |
swap trades a small layout shift for the guarantee that text is never invisible while waiting on a network request — the right default for body copy, which is most of what Outfit renders on this site. optional is the better choice for a font used only in a hero headline above the fold, where a swap after first paint would visibly jump; this site doesn't split fonts that finely because it uses exactly one family everywhere.
Why this is one file family, not a weight array
Outfit is a variable font — one file that encodes a continuous weight axis, rather than a separate static file per weight (400, 500, 600, 700...). The Outfit() call above never passes a weight option, which is what tells next/font to serve the variable file: every font-normal, font-medium, font-semibold and font-bold utility this site's components use resolves to the same two downloaded files, with the specific weight selected at render time via the font's own variation axis. Requesting static weights instead (weight: ["400", "600", "700"]) would trade that for one file per requested weight — more total bytes for a multi-weight site, but each individual file smaller, and no reliance on the browser's variable-font support (universal in every browser this site targets, but a real compatibility question for a much older support matrix).
This codebase never made that tradeoff explicitly — it fell out of not specifying weight at all — but it's the right default for a site using four weights of one family: one variable file undercuts four static ones on total bytes, and the two-file, 46,988-byte total measured above is smaller than four separate static weight files would produce for the same visual range.
next/font/google vs next/font/local
Everything above uses next/font/google, which downloads Outfit from Google's catalog at build time and self-hosts the result — the network trip to Google happens once, during next build, never at request time. next/font/local skips that entirely for a font file you already have on disk:
import localFont from "next/font/local";
const brand = localFont({
src: "./BrandFont.woff2",
variable: "--font-brand",
display: "swap",
});
Both produce the same shape of output — a generated CSS variable, self-hosted files, no render-blocking third-party request — the only difference is where the source file comes from. This site uses the Google variant because Outfit is a Google Fonts family; a licensed or custom typeface not on Google's catalog would use next/font/local with an identical variable/display API, feeding the same --font-sans token in globals.css either way.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
font-sans renders the browser default, not the intended font | --font-outfit was never set — the layout that calls Outfit() isn't an ancestor of the page, or outfit.variable wasn't added to a class list | Confirm the class carrying outfit.variable wraps every route (the root <body>, not a per-page layout) |
Adding a new --font-* token doesn't generate a utility | Tailwind v4 + Turbopack doesn't hot-reload @theme edits | Restart npm run dev after editing globals.css |
| A brief flash of the wrong font on every page load | display: "swap" is doing its job — this is the expected tradeoff, not a bug | Switch to "optional" only if the visible swap matters more than guaranteed-visible text |
Font file requests appear in the network tab pointed at fonts.gstatic.com | next/font/google was bypassed — a <link> tag or a CSS @import was added by hand alongside it | Remove the manual link; next/font already self-hosts everything it needs |
| Non-Latin characters render in the fallback font, not Outfit | subsets: ["latin"] intentionally excludes those glyphs | Add the needed subset ("latin-ext", "cyrillic", etc.) if the site actually needs it — each one adds to the two-file total above |
tailwind.config.js fontFamily edits have no effect | Tailwind v4 removed the JS config file — token definitions live in @theme in CSS now | Move the font-family definition into @theme, as --font-sans is defined here |
FAQ
Do I need tailwind.config.js to set a custom font in Tailwind v4?
No — v4 has no JS config file. Define the token in a @theme block in your CSS, as --font-sans is defined above, and reference it with next/font's generated CSS variable.
Is next/font faster than a Google Fonts <link> tag?
Yes, for two independent reasons: it self-hosts the files (no third-party connection to open before text can render) and it inlines the @font-face rule at build time instead of fetching a stylesheet that then fetches the font.
How many font files does a typical next/font setup produce?
It depends on subsets and weights requested — this site's single variable-font family with one subset produces two files. Requesting multiple static weights or additional subsets (Cyrillic, Greek) multiplies that count.
What does subsets: ["latin"] actually exclude?
Every glyph outside the Latin character set — Cyrillic, Greek, CJK, and others. It's a real byte-size lever: each additional subset is a separate file added to the total measured above.
Templates in this post
ASoc Mind is an AI creative-services landing page, ASoc Momentum an AI strategy-agency template, and ASoc Neuron a neural-network AI landing page — three templates built on the same next/font + @theme wiring measured above.
Browse the full set: Next.js landing page templates and Tailwind landing page templates. For the rest of the token system this font variable feeds into, see Tailwind design tokens.
