Webfonts in our Monorepo: Opt‑in Loading, Shared Utilities, Cross‑app Demo
We're adding a consistent, efficient way to demo and adopt multiple webfonts across both Next.js and Astro apps without shipping every font by default.
Goals
- Keep font loading opt‑in so pages only download the families they actually use.
- Provide consistent Tailwind utilities (e.g.,
.font-inter,.font-montserrat) that work in every app. - Support two integration paths:
- Next.js:
next/fontfor Google/OFL families. - Astro (and anywhere else):
@fontsourceCSS imports or self‑hosted files.
- Next.js:
- Enable a dedicated demo page that can showcase a curated subset of fonts.
Reference: “24 best fonts for websites” by Figma’s Resource Library — we’ll demo from this list and similar high‑quality families.
Figma: 24 best fonts for websites
Why opt‑in loading?
Loading 20+ families globally hurts performance and TTI, especially on mobile. An opt‑in model:
- Minimizes unused bytes (only download font files used on the page).
- Keeps Core Web Vitals healthy.
- Still allows a rich “fonts showcase” page when needed.
Architecture overview
- Tailwind package (
packages/tailwind)
- Defines utility classes only, like
.font-inter { font-family: var(--font-inter, ui-sans-serif), ... }. - Does not import any font files globally.
- This makes
.font-<name>classes safe to use anywhere without triggering network fetches unless the corresponding font is loaded by the page.
- Fonts package (proposed:
packages/fonts)
- Small CSS entrypoints per family, e.g.,
inter.css,montserrat.css,lora.css. - Each file:
- Imports the font (via
@fontsourceor self‑hosted@font-face) - Sets a CSS custom property like
--font-inter: "Inter Variable", Inter;
- Imports the font (via
- Apps import only the families they need on a given page (Astro) or use
next/font(Next.js) but still map to the same--font-*variables.
- Apps
- Next.js (
apps/web): usenext/font/googleand bindvariable: "--font-<name>". - Astro (
apps/astro-app): import@workspace/fonts/<family>.cssonly on pages that demo/use that family.
How to use in Next.js
Example: page‑level loading for a demo route.
// apps/web/app/fonts/page.tsx
import { Inter, Montserrat, Lora } from "next/font/google";
const inter = Inter({ subsets: ["latin"], variable: "--font-inter", display: "swap" });
const montserrat = Montserrat({ subsets: ["latin"], variable: "--font-montserrat", display: "swap" });
const lora = Lora({ subsets: ["latin"], variable: "--font-lora", display: "swap" });
export default function Page() {
const sample = "The quick brown fox jumps over the lazy dog. 0123456789";
return (
<main className={`${inter.variable} ${montserrat.variable} ${lora.variable} space-y-6`}>
<div className="font-inter text-2xl">{sample}</div>
<div className="font-montserrat text-2xl">{sample}</div>
<div className="font-lora text-2xl">{sample}</div>
</main>
);
}
- Only Inter, Montserrat, and Lora are downloaded on this page.
- Add/remove imports to adjust the subset you want to demo.
How to use in Astro
Example: page‑level imports with @fontsource via our fonts package entrypoints.
---
// apps/astro-app/src/pages/fonts.astro
const sample = "The quick brown fox jumps over the lazy dog. 0123456789";
---
<style is:global>
@import "@workspace/fonts/inter.css";
@import "@workspace/fonts/montserrat.css";
</style>
<main class="space-y-6">
<div class="font-inter text-2xl">{sample}</div>
<div class="font-montserrat text-2xl">{sample}</div>
</main>
- Only the imported families load for this page.
- Prefer variable fonts when available to reduce request count and enable weight/width tuning.
Tailwind utilities (shared, non‑loading)
We expose a consistent set of utilities in packages/tailwind/src/globals.css but do not import any font files there. Examples:
@layer utilities {
.font-inter { font-family: var(--font-inter, ui-sans-serif), system-ui, -apple-system, "Segoe UI", Roboto, Arial; }
.font-montserrat { font-family: var(--font-montserrat, ui-sans-serif), system-ui, -apple-system, "Segoe UI", Roboto, Arial; }
.font-lora { font-family: var(--font-lora, ui-serif), Georgia, Cambria, "Times New Roman", Times, serif; }
/* Add others as needed. These utilities don't trigger network fetches by themselves. */
}
Fonts package entrypoints (opt‑in CSS)
Each font gets its own CSS file:
/* packages/fonts/inter.css */
@import "@fontsource-variable/inter/index.css";
:root { --font-inter: "Inter Variable", Inter; }
/* packages/fonts/montserrat.css */
@import "@fontsource/montserrat/400.css";
:root { --font-montserrat: "Montserrat"; }
/* packages/fonts/lora.css */
@import "@fontsource/lora/400.css";
:root { --font-lora: "Lora"; }
This pattern works for most of the Figma list (Inter, Josefin Sans, Roboto, Open Sans, Rubik, DM Sans, Poppins, Lato, Nunito, Ubuntu, Source Sans 3, Work Sans, Manrope, Raleway, Montserrat, Playfair Display, Libre Baskerville, Neuton, Lora, Arvo). Some fonts (e.g., Ranade, Object Sans, Soria, Sreda) may require manual files and license checks before adding.
Demo strategy
- Create a single “Fonts” page per app that imports a curated subset (e.g., 6–8 families) to compare tone/readability.
- Add additional focused pages when evaluating alternatives (e.g., sans‑serif body vs. serif headings).
Performance and UX tips
- Use
display: "swap"(default fornext/font) or ensurefont-display: swapin@fontsourceto avoid FOIT. - Prefer variable fonts where available; one file often replaces multiple weights.
- Avoid importing entire families globally; stay page‑scoped.
- Define sane fallbacks in utilities to keep layout stable before custom fonts render.
Licensing notes
- Verify licensing for non‑Google families before bundling. If a font isn’t available via
@fontsourceor Google Fonts, self‑host only if your license allows it.
Summary
We keep fonts opt‑in and page‑scoped for performance while providing shared Tailwind utilities and a consistent variable‑based mapping that works in both Next.js and Astro. This lets us quickly demo and adopt fonts from reputable lists (like Figma’s “24 best fonts for websites”) without penalizing users who never see those pages.