Type something to search...
How to Pair Fonts: Rules for Combining Two Typefaces

How to Pair Fonts: Rules for Combining Two Typefaces

You've chosen a body font you like, and now you need something for headings. You scroll through Google Fonts, try a dozen candidates, and each one looks fine on its own specimen page. But on your actual homepage, the headline font either looks so similar to the body text that it seems like a mistake, or so different that the page feels like two websites stitched together. Font pairing isn't about finding two good fonts. It's about finding two fonts that do different jobs and still look like they belong to the same design.

This article gives you a set of practical rules for combining two typefaces: how to create contrast without conflict, which structural features to match, how to measure and equalize x-heights, and how to test a pairing with real content. It finishes with a table of proven pairs and the code to implement a pairing in CSS, Next.js, and Tailwind CSS v4.

What Font Pairing Is Trying to Achieve

A font pairing assigns two typefaces to different roles, usually:

  • Display or heading font: sets headlines, section titles, and sometimes pull quotes. Seen in short bursts, often at large sizes.
  • Text or body font: sets paragraphs, lists, captions, and UI text. Read for long stretches at small sizes.

A good pairing makes those roles obvious at a glance, which supports typographic hierarchy, while giving the page a single coherent voice. If you aren't sure whether you need two typefaces at all, read how many fonts a website should use first. One well-chosen family with several weights is often the better answer.

Rule 1: Contrast, Not Conflict

The two fonts need to differ enough that the difference looks intentional. Two similar geometric sans-serifs, like Montserrat and Poppins, look like an accident: readers notice something is slightly off but can't say what.

Good sources of contrast:

Contrast typeExample
ClassificationSerif headings with sans-serif body text
WeightHeavy display headings, regular body
WidthCondensed headlines, normal-width body
Stroke contrastHigh-contrast Didone headings, low-contrast sans body
PersonalityExpressive display face, neutral text face

Pick one or two dimensions of contrast and keep the rest similar. If the fonts differ in classification, weight, width, and personality all at once, you get conflict instead of contrast. The most reliable starting point is the classic serif and sans-serif pairing, explained in more depth in serif vs. sans-serif fonts.

Rule 2: Match the Underlying Structure

While the surfaces differ, the skeletons should agree. Look at:

  • X-height: the height of lowercase letters relative to capitals. Fonts with similar x-height ratios look the same size when set at the same font-size.
  • Proportions: are the letters wide and round, or narrow and upright? A wide geometric sans pairs better with a wide, round serif than with a narrow, angular one.
  • Construction: humanist fonts (based on pen strokes, like Source Sans 3 or Lora) share a calligraphic logic. Geometric fonts (based on circles and straight lines, like Jost, Futura, or Poppins) share a constructed logic. Matching the construction is one of the most reliable ways to make two classifications feel related.
  • Aperture: the openness of letters like c, e, and a. Open apertures in one font and tightly closed ones in the other can look inconsistent.

A quick test: set "Hamburgefonstiv" (a classic type-testing word) in both fonts at the same size and look at the lowercase. If the a, e, g, and o feel like they were drawn by people with similar ideas, the structure is compatible.

Rule 3: Measure and Equalize X-Heights

You don't need to guess at x-height. Fonts store it in the OS/2 table, and you can read it with fontTools:

pip install fonttools brotli

python3 - <<'EOF'
from fontTools.ttLib import TTFont

for path in ["SourceSerif4-Regular.ttf", "SourceSans3-Regular.ttf"]:
    f = TTFont(path)
    upm = f["head"].unitsPerEm
    xh = f["OS/2"].sxHeight
    cap = f["OS/2"].sCapHeight
    print(f"{path}: x-height {xh / upm:.3f} em, cap height {cap / upm:.3f} em")
EOF

If the ratios differ by more than about 0.03 em, the fonts will look mismatched in size, especially when they appear in the same line, such as a sans-serif label next to serif text.

CSS can correct this. font-size-adjust scales a font so its x-height equals a specified fraction of the font-size, and it's supported in all modern browsers:

:root {
  --font-heading: "Fraunces", Georgia, serif;
  --font-body: "Inter", system-ui, sans-serif;
}

body {
  font-family: var(--font-body);
}

h1,
h2,
h3 {
  font-family: var(--font-heading);
}

/* Inline serif inside sans text, normalized to the same x-height */
.serif-inline {
  font-family: var(--font-heading);
  font-size-adjust: ex-height 0.53;
}

Use font-size-adjust mostly where both fonts share a line. For headings that sit on their own lines, a small font-size adjustment in your type scale is simpler.

Rule 4: Give Each Font One Clear Job

Assign roles and stick to them. A common failure is letting the display font creep into places it wasn't designed for: buttons, form labels, navigation, or body text.

A simple role map:

ElementFont
H1–H3Heading font
H4–H6Body font, bold or semibold
Body, lists, tablesBody font
Buttons, navigation, formsBody font
Pull quotesHeading font, italic if available
Captions, metadataBody font, smaller size

Using the body font for smaller headings is a useful trick. Display fonts often lose their character and legibility at small sizes, and switching to the body font at H4 keeps minor headings readable.

Rule 5: Use Superfamilies When in Doubt

A superfamily is a set of families designed together, such as a sans, a serif, and sometimes a mono, all sharing proportions and metrics. They're the safest pairing because the structural matching has already been done.

Well-known free options:

  • IBM Plex: Plex Sans, Plex Serif, Plex Mono, plus condensed and non-Latin versions.
  • Source: Source Sans 3, Source Serif 4, Source Code Pro.
  • Noto: Noto Sans and Noto Serif, with coverage for most of the world's scripts.
  • DM: DM Sans, DM Serif Display, DM Serif Text, DM Mono.
  • Roboto: Roboto, Roboto Serif, Roboto Mono, Roboto Slab.

The trade-off is that superfamily pairings can feel safe rather than distinctive. That's often exactly right for product and documentation sites.

Rule 6: Respect Historical and Stylistic Context

Fonts carry associations from the era and context they come from. A 1950s grotesque, a Renaissance-style serif, and a 2020s variable display face each bring a flavor. Pairing fonts from compatible contexts usually works better than mixing eras randomly.

That doesn't mean they must come from the same period. Plenty of excellent pairings deliberately combine a classical serif with a modern sans. But the combination should feel like a choice. If you can't explain why the two belong together in a sentence, the pairing probably doesn't work.

Rule 7: Test with Real Content

Specimen pages show fonts at their best. Your site will show them with your content, your colors, and your layout. Before committing:

  1. Use a real page, not lorem ipsum. Include a long headline that wraps, a short one, body paragraphs, a list, a link, a button, and a table.
  2. Test at your actual sizes. Pairings that work at 72px and 18px can fall apart at 32px and 15px.
  3. Check on Windows and a low-density screen, not only a Retina Mac. Thin strokes and fine serifs behave differently.
  4. Squint test. Blur your eyes or apply a blur filter. The headings should still clearly read as headings.
  5. Check weights and italics. Make sure both families have the styles you need, such as an italic for the body font. A missing italic forces the browser to fake one.
  6. Check performance. Two families with several weights each add up. Count the files in DevTools.

Proven Google Fonts Pairings

These pairs are all available on Google Fonts and follow the rules above. Treat them as starting points to test, not guaranteed answers.

HeadingsBodyWhy it worksGood for
FrauncesInterSoft, warm serif against a neutral, highly legible sansSaaS, blogs, product sites
Playfair DisplaySource Sans 3High-contrast display serif with a humanist sansEditorial, lifestyle
DM Serif DisplayDM SansSame superfamily, shared geometric proportionsStartups, portfolios
IBM Plex SansIBM Plex SerifSuperfamily; sans headings with a serif for readingDocumentation, long reads
Space GroteskSource Serif 4Quirky grotesque headings with a sober text serifTech blogs, agencies
Libre BaskervilleSource Sans 3Classical serif warmth with a clean modern bodyProfessional services

If you prefer the body text in a serif, flip the roles: a sturdy serif like Source Serif 4 or Literata for paragraphs, with a sans like Inter for headings and UI. For more single-font candidates, see the guides to the best Google Fonts for body text and headlines.

Implementing and Reviewing a Pairing

Plain CSS with Custom Properties

Keep the font stacks in custom properties so swapping a pairing is a two-line change:

:root {
  --font-heading: "Playfair Display", Georgia, "Times New Roman", serif;
  --font-body: "Source Sans 3", system-ui, -apple-system, "Segoe UI", sans-serif;
}

body {
  font-family: var(--font-body);
  line-height: 1.6;
}

h1,
h2,
h3,
blockquote {
  font-family: var(--font-heading);
  line-height: 1.2;
}

Choose fallbacks with a similar classification and width, so the page doesn't collapse into a mismatched layout if a web font fails.

Next.js with next/font

Load both families once and expose them as CSS variables:

// app/fonts.ts
import { Fraunces, Inter } from "next/font/google";

export const fraunces = Fraunces({
  subsets: ["latin"],
  display: "swap",
  variable: "--font-fraunces",
});

export const inter = Inter({
  subsets: ["latin"],
  display: "swap",
  variable: "--font-inter",
});
// app/layout.tsx
import { fraunces, inter } from "./fonts";
import "./globals.css";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en" className={`${fraunces.variable} ${inter.variable}`}>
      <body>{children}</body>
    </html>
  );
}

next/font also generates a size-adjusted fallback font for each family, which reduces layout shift when the web fonts load.

Tailwind CSS v4

Map the variables to theme tokens so you get font-heading and font-body utilities:

/* app/globals.css */
@import "tailwindcss";

@theme inline {
  --font-heading: var(--font-fraunces), Georgia, serif;
  --font-body: var(--font-inter), system-ui, sans-serif;
}

@layer base {
  body {
    font-family: var(--font-body);
  }

  h1,
  h2,
  h3 {
    font-family: var(--font-heading);
  }
}

@theme inline makes the utilities reference the next/font variables directly, so the values resolve on the element where the font classes are applied.

Common Pairing Mistakes

  • Two display fonts. Two expressive faces compete for attention. One should always be quiet.
  • Near-identical fonts. Two geometric sans-serifs or two transitional serifs look like an error.
  • Mismatched moods. A playful rounded display font on a serious financial site, or a stiff corporate sans for a children's brand.
  • Ignoring weights. A thin display font over a heavy body font inverts the hierarchy.
  • Different x-heights in the same line. Labels and inline code in a second font look too big or too small without adjustment.

Font Pairing FAQ

No, but it is the most reliable starting point because the contrast is obvious. Two sans-serifs can work if they differ clearly in width, weight, or personality, and a superfamily can pair a sans and serif that already share proportions.

Two families is the practical maximum for most sites, with a monospace font as an optional third for code. Many excellent sites use a single family and create hierarchy with weight and size alone.

Yes. What matters is structural compatibility, not who made the fonts. Check x-height, proportions, and construction, and test both fonts together on a real page rather than relying on specimens.

A superfamily such as IBM Plex, Source, or Noto is the safest choice, because the sans and serif were designed with matching metrics. They may feel less distinctive, but they almost never look wrong.

Compare their x-heights, which you can read from each font's OS/2 table. If they differ, adjust font sizes in your type scale, or use the CSS font-size-adjust property where both fonts appear in the same line.

It adds requests and bytes, so keep the number of weights low. Two families with two or three styles each, subset to the characters you need and served as WOFF2, usually add a manageable amount, especially with next/font or self-hosting.

Conclusion

Pairing fonts well comes down to a few principles: create contrast on one or two clear dimensions, match the underlying structure, keep x-heights compatible, and give each font a distinct job. Superfamilies are a safe shortcut, and historical or stylistic context helps you choose combinations that feel deliberate.

Then test with real content at real sizes on real screens, and implement the pairing through custom properties or theme tokens so it's easy to adjust later. A pairing that survives a real page, a Windows laptop, and a squint test will serve you better than one that only looked good on a specimen.

Here are some useful references for going deeper on font pairing:

  1. Google Fonts Knowledge: Google Fonts Knowledge — lessons on choosing and combining typefaces.
  2. Butterick's Practical Typography: Font recommendations — opinionated guidance on choosing and mixing fonts.
  3. MDN Web Docs: font-size-adjust — normalizing x-heights between fonts.
  4. Next.js Docs: Font Optimization — loading multiple fonts with next/font.
  5. Tailwind CSS Docs: font-family — defining custom font utilities with theme variables.
Share :

Related Posts

Ascenders, Descenders, and Baselines: The Anatomy of a Letterform

Ascenders, Descenders, and Baselines: The Anatomy of a Letterform

You align an icon next to a button label and it looks a pixel or two too high, no matter how you adjust vertical-align. You set overflow: hidden

Continue Reading
Are Google Fonts GDPR-Compliant?

Are Google Fonts GDPR-Compliant?

In late 2022, thousands of small business owners in Germany and Austria opened letters demanding a few hundred euros in "damages" because their websi

Continue Reading
How to Audit Web Font Performance with Lighthouse?

How to Audit Web Font Performance with Lighthouse?

A client sends you a screenshot of their PageSpeed Insights report: performance score 61, LCP 3.9 seconds, and a vague list of warnings. They want to

Continue Reading