Skip to main content
ASoc
Tutorial

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.

The ASoc Team9 min read

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 valueBefore the font loadsAfter it loads
swap (this site)Fallback font (ui-sans-serif, system-ui, sans-serif) renders immediatelySwaps 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
optionalFallback renders; browser may skip the swap entirely on a slow connectionOnly 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

SymptomCauseFix
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 listConfirm 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 utilityTailwind v4 + Turbopack doesn't hot-reload @theme editsRestart npm run dev after editing globals.css
A brief flash of the wrong font on every page loaddisplay: "swap" is doing its job — this is the expected tradeoff, not a bugSwitch 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.comnext/font/google was bypassed — a <link> tag or a CSS @import was added by hand alongside itRemove the manual link; next/font already self-hosts everything it needs
Non-Latin characters render in the fallback font, not Outfitsubsets: ["latin"] intentionally excludes those glyphsAdd 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 effectTailwind v4 removed the JS config file — token definitions live in @theme in CSS nowMove 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.

Keep reading

Tutorial10 min read

Tailwind Font Weight: 9 Named Steps, 4 This Codebase Uses

193 font-weight utility calls across this codebase, spanning only 4 of Tailwind's 9 named steps — traced to two atoms that set the hierarchy once.

Read more
Tutorial9 min read

Tailwind Grid: 26 Files, and Not One col-span

A 538-page site's whole grid vocabulary is three utilities — plus the display:contents trick that reorders a product page on mobile without duplicating state.

Read more