Font Pairing Guide: What Fonts Go Together in HTML & CSS?

A blunt font pairing guide for HTML and CSS: when to use one family, five combinations worth trying, and copy-paste code for Next.js and Tailwind CSS.

March 16, 20269 min readBy Yann Lephay
TL;DR

For product UI, start with one variable sans-serif and create hierarchy with size, weight, and spacing. Add a second family only when it has a clear job: usually expressive headings against a quiet body font.

You do not need two fonts to make a website look designed.

That is the first rule most font-pairing galleries avoid because a gallery needs pairs to sell you. A product interface often needs the opposite: fewer font files, fewer arbitrary decisions, and a hierarchy that still works when the novelty wears off.

For a dashboard or SaaS app, start with one good variable sans-serif. Use weight, size, line height, and spacing to separate headings from body copy. Add a second family only when you can finish this sentence:

The second font exists to ________.

“Make it less boring” is not a role. “Give marketing headlines an editorial voice while the product UI stays neutral” is.

If you want to compare the options visually before touching code, use the free font pairing generator. If you want the decision rule first, keep reading.

The short answer: which fonts go together?

What you are buildingStart hereWhy
SaaS product or dashboardInter aloneOne family can cover dense UI, labels, numbers, and headings without manufacturing contrast.
Developer tool or technical landing pageSpace Grotesk + InterThe heading face has a point of view; the body face gets out of the way.
Fintech, research, or editorial productFraunces + ManropeThe serif adds warmth to large text; the sans-serif keeps controls and data quiet.
Creative tool or indie productBricolage Grotesque + Source Sans 3The display face is distinctive without forcing the body copy to perform.
Documentation or reading-heavy siteSource Serif 4 + Source Sans 3The two families keep long reading and navigation in distinct roles.

These are starting points, not laws. A font pair is not “good” in isolation. It is good when it survives your words, your languages, your numbers, your screen sizes, and your loading strategy.

A font pairing needs roles, not chemistry

Forget the vague advice about fonts “complementing each other.” Assign jobs.

  • Display font: hero headings, campaign statements, perhaps section titles.
  • Body font: paragraphs, navigation, forms, buttons, tables, prices, and error messages.
  • Monospace accent: code, IDs, timestamps, or technical metadata — not the entire interface.

The body font carries more of the product, so choose it first. Test lowercase text, accented characters, 0/O, 1/l/I, tabular numbers, punctuation, and the ugliest real error message in your app. A beautiful uppercase hero tells you almost nothing about whether a font works in a billing table.

Then decide whether the body family can also handle headings. If it can, stop. You have a system.

Five combinations worth testing

1. Inter alone — the honest default for product UI

Inter is not a dramatic choice. That is the point. For dashboards, admin tools, and products where users spend hours rather than seconds, typographic calm beats a forced brand moment on every card title.

Use one family and create contrast with a restrained scale:

Code
:root {
  --font-ui: "Inter", system-ui, sans-serif;
}
 
body {
  font-family: var(--font-ui);
  font-size: 1rem;
  line-height: 1.6;
}
 
h1,
h2,
h3 {
  font-family: var(--font-ui);
  font-weight: 700;
  letter-spacing: -0.025em;
}

Choose it for: dashboards, B2B software, settings-heavy products, internal tools.

Do not choose it for: a marketing site whose typography is supposed to carry most of the brand personality.

2. Space Grotesk + Inter — the developer-tool pair

Space Grotesk has enough irregularity to make a headline recognizable. Inter is calmer at interface sizes. The division of labour is obvious, which is why the pair works better than two almost-identical geometric sans-serifs.

Choose it for: APIs, developer tools, infrastructure products, technical SaaS.

Watch for: overusing Space Grotesk in buttons and body copy. If everything speaks loudly, nothing does.

3. Fraunces + Manrope — warmth without a costume drama

Fraunces gives large headings a softer, more editorial rhythm. Manrope handles navigation, UI, prices, and explanatory copy without imitating the serif.

Choose it for: fintech with a human voice, research products, premium services, editorial commerce.

Watch for: small Fraunces text. Keep the serif in the sizes where its character is an advantage.

4. Bricolage Grotesque + Source Sans 3 — expressive landing, quiet product

Bricolage Grotesque is a display decision. Source Sans 3 is a working decision. That contrast is useful when the landing page must feel authored but the application still needs to be scanned quickly.

Choose it for: creative software, indie launches, portfolio products.

Watch for: stacking personality on top of personality. Let Source Sans 3 own forms, tables, and long copy.

5. Source Serif 4 + Source Sans 3 — for actual reading

If the core experience is documentation, reports, essays, or a knowledge base, a serif body can be reasonable. Source Serif 4 and Source Sans 3 give you distinct reading and navigation roles without turning the site into a type specimen.

Choose it for: documentation, editorial tools, research libraries, long-form publishing.

Watch for: importing more styles than the page uses. Test italic, bold, code, footnotes, and tables before calling the system complete.

Copy-paste HTML and CSS example

This example uses Space Grotesk for headings and Inter for everything operational. The Google Fonts CSS API supports multiple families and variable-weight ranges. A browser-loaded stylesheet is convenient for a prototype; for production, also review privacy requirements, caching, and whether self-hosting is the better fit.

Code
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
  href="https://fonts.googleapis.com/css2?family=Inter:wght@400..700&family=Space+Grotesk:wght@500..700&display=swap"
  rel="stylesheet"
/>
 
<main class="page-shell">
  <p class="eyebrow">Deployment preview</p>
  <h1>Ship the system, not another moodboard.</h1>
  <p class="lede">
    A display font gives the promise a voice. The body font keeps the product usable.
  </p>
  <button type="button">Create a preview</button>
</main>
Code
:root {
  --font-display: "Space Grotesk", system-ui, sans-serif;
  --font-body: "Inter", system-ui, sans-serif;
}
 
body,
button,
input,
textarea {
  font-family: var(--font-body);
}
 
h1,
h2,
h3 {
  font-family: var(--font-display);
  font-weight: 650;
  letter-spacing: -0.035em;
}
 
.lede {
  max-width: 62ch;
  font-size: 1.125rem;
  line-height: 1.65;
}

The important part is not the family names. It is the boundary: display typography belongs to headings; operational typography owns everything a user must read or touch.

Next.js and next/font without the CDN request

The official next/font documentation says Google font files are downloaded at build time and self-hosted with the application. For variable fonts, you do not need to enumerate every weight.

Code
// app/layout.tsx
import { Inter, Space_Grotesk } from 'next/font/google';
import './globals.css';
 
const display = Space_Grotesk({
  subsets: ['latin'],
  variable: '--font-display-face',
  display: 'swap',
});
 
const body = Inter({
  subsets: ['latin'],
  variable: '--font-body-face',
  display: 'swap',
});
 
export default function RootLayout({
  children,
}: Readonly<{ children: React.ReactNode }>) {
  return (
    <html lang="en" className={`${display.variable} ${body.variable}`}>
      <body>{children}</body>
    </html>
  );
}
Code
/* app/globals.css */
:root {
  --font-display: var(--font-display-face), system-ui, sans-serif;
  --font-body: var(--font-body-face), system-ui, sans-serif;
}
 
body {
  font-family: var(--font-body);
}
 
h1,
h2,
h3 {
  font-family: var(--font-display);
}

Do not add a second font import just because the tutorial has one. If Inter alone does the job, import Inter alone.

Tailwind CSS v4: map the variables in CSS

Tailwind CSS v4 defines custom font utilities through --font-* theme variables. That means this belongs in CSS, not in an old tailwind.config.ts copied from a v3 article. The syntax below follows the current Tailwind font-family documentation.

Code
@import "tailwindcss";
 
@theme inline {
  --font-display: var(--font-display-face);
  --font-sans: var(--font-body-face);
}
Code
export function Hero() {
  return (
    <section>
      <p className="font-sans text-sm font-semibold uppercase tracking-wider">
        Developer-first branding
      </p>
      <h1 className="font-display text-5xl font-bold tracking-tight">
        Pick fonts once. Go back to shipping.
      </h1>
    </section>
  );
}

If your project still uses Tailwind v3, its JavaScript theme configuration is valid for that project. Do not mix the two setup styles blindly.

The 10-minute test that kills bad pairs

Do not judge a pairing from “The quick brown fox.” Put it through your product.

  1. Render the real hero, the longest navigation label, a pricing card, a form error, and a dense table row.
  2. Test regular, medium, semibold, bold, italic, numbers, punctuation, and the languages you actually ship.
  3. View it on a narrow phone and a wide desktop. A display face that only works at 72px is not a heading system.
  4. Disable the web font in DevTools and inspect the fallback. The layout should bend, not break.
  5. Check the network panel. Load only the families, subsets, and styles the interface uses.
  6. Read the font licence before shipping, especially for local or commercial files.

If one family passes and the second adds no clear role, remove the second. That is not giving up on design. That is editing.

Common font-pairing mistakes

Pairing two fonts that do the same job

Two geometric sans-serifs with similar proportions usually create administrative complexity, not useful contrast. Keep the stronger one.

Using monospace as a personality shortcut

Monospace can signal code, but setting every paragraph in mono makes reading feel like work. Use it for code and compact metadata.

Treating a font name as a brand system

Typography also includes scale, weight, line height, measure, spacing, and the rules that decide where each style appears. A fashionable family with no usage rules is still an unfinished system.

Copying a Tailwind v3 config into Tailwind v4

The pair may be fine while the implementation is stale. Check the version your project runs, then use the matching documentation.

Claiming that a pair “always works”

No pair always works. It can lack the required glyphs, fail in data-heavy UI, clash with an existing logo, or cost more to load than the distinction is worth. Recommendations need a context and a counter-case.

My default recommendation

For a SaaS product, use one variable sans-serif first. Inter is the boring answer because boring is often correct inside the product.

If the marketing layer genuinely needs a voice, add Space Grotesk for headings and keep Inter for body and UI. That is the pair I would try first for a developer tool — not because it is mathematically perfect, but because the roles are obvious and easy to police in code.

Then stop browsing fonts. Use the font pairing generator to preview alternatives against real UI, or generate a complete developer-ready brand system when typography is only one of the decisions blocking the launch.

Y
Yann Lephay@YannBuilds

Vibe coder & Indie Hacker. Building tools to help devs ship faster. Creator of OneMinuteBranding.