Swap the font
Use this when the user wants to change the site's font: "use Inter", "switch to a serif", "I want Playfair Display headings", "make the body sans and headings monospace", and so on.
The goal: the new font is loaded properly, applied via the existing Tailwind v4 token system, and DESIGN.md stays in sync.
Rules of engagement
- Update token values only in
app/styles/app.css(@theme { }) and the matching section inDESIGN.md. Do NOT add inlinefont-familyoverrides on individual components. - Prefer a single sans for body and a separate display font only when the user explicitly asks. Avoid more than two font families on the same site.
- Pull from Google Fonts by default unless the user names a specific provider or self-hosted file.
1. Identify the change
Ask the user:
- Body font (sans by default)?
- Heading / display font (only if they want a separate one)?
- Mono font (for code blocks, usually system mono is fine)?
- Any specific weights they need (e.g.,
400, 500, 700)?
If they only say "use a serif" or similar, pick a sensible default (e.g., Source Serif, IBM Plex Serif, Playfair Display) and confirm with them before installing.
2. Add the Google Fonts link
In app/routes/__root.tsx, find the links array in the head() config and add a stylesheet for the new font(s). Example for Inter (body) + Playfair Display (headings) with weights 400/500/600/700:
links: [
{ rel: 'stylesheet', href: appCss },
// ...existing favicon links if any
{ rel: 'preconnect', href: 'https://fonts.googleapis.com' },
{ rel: 'preconnect', href: 'https://fonts.gstatic.com', crossOrigin: '' },
{
rel: 'stylesheet',
href: 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Playfair+Display:wght@600;700&display=swap',
},
],Always include display=swap and only the weights you need (smaller payload).
3. Update the design tokens
Open app/styles/app.css. Inside @theme { }, update the font tokens:
@theme {
--font-sans: 'Inter', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
--font-mono: ui-monospace, 'SF Mono', Menlo, Monaco, Consolas, monospace;
/* If the user wants a separate heading font, add: */
--font-display: 'Playfair Display', Georgia, serif;
}If the user opted for a separate display font, also wire it into headings. The cleanest way: open app/styles/app.css base styles and add:
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-display, var(--font-sans));
}Without a --font-display, headings inherit --font-sans and nothing changes.
4. Sync DESIGN.md
Open DESIGN.md at the repo root. Find the Typography section and update the font names to match:
## Typography
- Sans: `Inter`, system fallbacks
- Mono: system mono stack (`SF Mono`, `Menlo`, `Consolas`)
- Display (headings): `Playfair Display`, Georgia, serifKeep the rest of the typography section (sizes, weights, leading) as-is unless the user asked for different scale.
5. Verify
- Hot reload should kick in. Open the site in the browser.
- Check that body text uses the new sans, headings render in the display font (if separate), and code blocks still use mono.
- Network tab in DevTools: confirm only the requested weights download, not the full family.
- Lighthouse: a font swap should not regress LCP. If it does, drop unused weights.
6. Common pitfalls
- Forgetting
display=swap— page shows invisible text until font loads. Always include it. - Loading too many weights — every weight is a separate file. Stick to 2-3.
- Self-hosted custom font (.woff2) — out of scope here. If the user has one, ask for the file paths and weights, then add
@font-facerules inapp/styles/app.cssand reference the family name in the--font-*token. - Keeping the old font in
DESIGN.md—DESIGN.mdandapp.cssmust say the same thing or future agents will get confused. Always update both.