Labels.io
Agent skills

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 in DESIGN.md. Do NOT add inline font-family overrides 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.

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, serif

Keep 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-face rules in app/styles/app.css and reference the family name in the --font-* token.
  • Keeping the old font in DESIGN.mdDESIGN.md and app.css must say the same thing or future agents will get confused. Always update both.

On this page