Fonts, paired with care
59 Google Fonts families in 7 categories, 16 curated pairings, and a loader that requests only what a page uses. Every family covers latin-ext, so Turkish ğ ş ı İ render in the chosen face.
| Category | Families | Use for |
|---|---|---|
| Sans | 23 | Geometric, humanist and grotesk faces for UI and body text |
| Serif | 13 | Text serifs for long reads, display serifs for headlines |
| Display | 4 | Posters, campaigns, one-word headlines |
| Rounded | 4 | Friendly products, education, health |
| Condensed | 5 | Tight headlines, stat-heavy headers, dense tables |
| Mono | 7 | Code, IDs, invoice numbers, tabular figures |
| Handwriting | 3 | Annotations and small accents — never body text |
Catalog
Every row is set in its own face. Open one to load the full family and see each weight. The list comes from googleFonts, with weights, italics, variable axes, subsets and a short note per family — verified against the Google Fonts metadata.
Sans · Geometric7
Sans · Humanist7
Sans · Grotesk9
Serif · Text7
Serif · Display6
Display4
Rounded4
Condensed5
Mono7
Handwriting3
Pairings
A pairing is a heading face, a body face, a mono face and the character of display headings: weight, tracking, how <em> looks and a size scale (wide faces need less size). editorial, grotesk, corporate and system are also data-font presets in styles.css.
Condensed serif headlines over a warm sans. The house style. Çalışma takvimi, izin talepleri ve bordro özetleri aynı yerde.
Tokens
| Token | Set from | Read by |
|---|---|---|
--font-ui | pairing.body | font-sans (body, controls) |
--font-heading | pairing.heading | font-display |
--font-code | pairing.mono | font-mono, tabular numbers |
--display-weight · --display-tracking | pairing.display | .font-display |
--display-em | pairing.display.em | .font-display em |
--display-scale | pairing.display.scale | --text-display-* sizes |
Loading fonts
loadGoogleFonts adds one <link rel="stylesheet"> for the families a page has not requested yet, plus preconnect hints for both Google origins. Calling it again with the same families does nothing.
import { loadGoogleFonts, applyFontPairing, getFontPairing } from "@vamos-ui/react/fonts";
await loadGoogleFonts(["Fraunces", "Figtree"], { display: "swap" });
// Or apply a whole pairing to <html> (data-font + tokens + fonts):
await applyFontPairing(document.documentElement, getFontPairing("boutique")!);On the server, render the link yourself with the same URL:
import { googleFontsUrl } from "@vamos-ui/react/fonts";
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
<link rel="stylesheet" href={googleFontsUrl(["Newsreader", "Source Sans 3"])} />For one section only — a website preview inside the CMS, an email template — wrap it in FontProvider. The rest of the panel keeps its fonts.
import { FontProvider, createFontPairing } from "@vamos-ui/react/fonts";
const brand = createFontPairing({ heading: "Young Serif", body: "Plus Jakarta Sans", mono: "JetBrains Mono" });
<FontProvider pairing={brand}>
<HomepagePreview />
</FontProvider>Font picker
FontPicker and FontPairingPicker let editors choose. The theme switcher in this site's header uses both: pick a pairing, or open Custom and mix any three families.
No flash on reload
Store the resolved pairing with serializeFontPairing; it keeps the stylesheet URL and the token values, so a tiny inline script can apply them before first paint without shipping the catalog. The link is marked blocking="render", so supporting browsers wait for the CSS (not the font files) and font-display: swap covers the rest.
import { fontBootScript, serializeFontPairing } from "@vamos-ui/react/fonts";
// When the user picks:
localStorage.setItem("vamos-theme", JSON.stringify({ ...theme, fontPairing: serializeFontPairing(pairing) }));
// app/layout.tsx
<head>
<script dangerouslySetInnerHTML={{ __html: fontBootScript("vamos-theme", "fontPairing") }} />
</head>Privacy
fonts.googleapis.com and fonts.gstatic.com sends each visitor's IP address and user agent to Google. Under GDPR and KVKK that is a transfer of personal data; a German court has fined a site for it. For public sites, self-host the files and keep the CDN for internal tools and the picker in your admin.Self-hosted fonts also skip two connections. Turn the CDN off with load={false} on FontProvider and loadPreviews={false} on the pickers.
Self-hosting with next/font
next/font/google downloads the files at build time and serves them from your domain — no request reaches Google at runtime. Define the three families, expose them as CSS variables and point the tokens at them.
import { Fraunces, Figtree, DM_Mono } from "next/font/google";
// latin-ext keeps ğ ş ı İ in the same face.
export const heading = Fraunces({ subsets: ["latin", "latin-ext"], style: ["normal", "italic"], axes: ["opsz"], variable: "--ff-heading" });
export const body = Figtree({ subsets: ["latin", "latin-ext"], variable: "--ff-body" });
export const mono = DM_Mono({ subsets: ["latin", "latin-ext"], weight: ["400", "500"], variable: "--ff-mono", preload: false });import { heading, body, mono } from "./fonts";
import { fontPairings, pairingVars } from "@vamos-ui/react/fonts";
const boutique = fontPairings.find((p) => p.id === "boutique")!;
// Keep the pairing's display tokens, swap the families for the self-hosted variables.
const vars = { ...pairingVars(boutique), "--font-heading": "var(--ff-heading)", "--font-ui": "var(--ff-body)", "--font-code": "var(--ff-mono)" };
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" data-font="boutique" style={vars as React.CSSProperties} className={[heading, body, mono].map((f) => f.variable).join(" ")}>
<body className="font-sans">{children}</body>
</html>
);
}Not on Next.js? Download the woff2 files (the URLs are in the CSS that googleFontsUrl returns), serve them from your own origin with @font-face rules, and set the same three tokens.