Font Pairing Generator
Fourteen font pairings that work, set in your own words, with the CSS, the theme.json or the link tags to put them on a site.
Enable JavaScript to try the other pairings and to see the faces themselves; without it the preview shows the fallback stack.
Heading face first, body face second. Every one is on Google Fonts.
Your own words, because a pairing that looks right in Latin filler can fall apart in a real headline.
The size before the correction. A face with a small x-height is set larger than this, and one with a large x-height smaller.
h1. h2 and h3 follow at 68 and 50 percent of it.
The CSS carries an @import. For a production site use the link tags instead: an @import blocks until it resolves.
Editorial
Ship the thing that reads well
A paragraph long enough to judge the body face by, which means more than one line of it. What you are looking for is whether the words hold together as shapes at this size, and whether the heading above looks like it belongs to the same page rather than to a different one.
Editorial. The magazine default. High contrast strokes in the heading, a neutral workhorse underneath.
- Playfair Display 700
- ABCDEFGHIJKLM abcdefghijklm 0123456789
- Source Sans 3 400
- ABCDEFGHIJKLM abcdefghijklm 0123456789
/* Playfair Display + Source Sans 3 — Editorial */
@import url("https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700&family=Source+Sans+3:wght@400;600&display=swap");
:root {
--font-heading: "Playfair Display", Georgia, "Times New Roman", serif;
--font-body: "Source Sans 3", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
body {
font-family: var(--font-body);
font-size: 17px;
font-weight: 400;
line-height: 1.6;
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: var(--font-heading);
font-weight: 700;
line-height: 1.15;
letter-spacing: -0.01em;
}
h1 {
font-size: 2.5rem;
}
h2 {
font-size: 1.7rem;
}
h3 {
font-size: 1.25rem;
}
Most font pairing tools let you cross twenty heading faces with twenty body faces, which mostly produces four hundred bad pairs. This one has fourteen, each picked for a job, each with the weights it needs and the size correction that keeps the body text from looking two sizes smaller than it is.
Set your own headline in it. A pairing that looks right in Latin filler can fall apart in a real one.
How to use
- Pick a pairing. The name is the heading face first, the body face second, and the line under the preview says what it is for and what to watch out for.
- Put your own headline in Heading text. Real words, with your real capitalisation.
- Set the body size and the heading size. The heading size is
h1;h2andh3follow at 68 and 50 percent of it, which is close enough to a 1.5 scale to be predictable. - Take the CSS, or switch to
theme.jsonfor a block theme, or to the link tags if you are putting the fonts in a<head>yourself.
The preview loads the two families from Google Fonts once the page is interactive. Until then it shows the fallback stack, which is what your visitors see while the fonts are still in flight.
Example
Fraunces + Inter, at a 17px body:
/* Fraunces + Inter — Editorial */
@import url("https://fonts.googleapis.com/css2?family=Fraunces:wght@700&family=Inter:wght@400;600&display=swap");
:root {
--font-heading: "Fraunces", Georgia, "Times New Roman", serif;
--font-body: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
body {
font-family: var(--font-body);
font-size: 16.5px;
font-weight: 400;
line-height: 1.6;
}
Two things in there are not the obvious choice.
The body size is 16.5px, not 17. Inter has a large x-height, so it reads a little larger than its nominal size; setting it slightly smaller makes it match the 17px you asked for. Libre Baskerville goes the other way, to 17.5px.
The heading gets letter-spacing: -0.015em. Display faces are drawn at large
sizes and spaced for them, so at 40px they still look loose. The correction is
per pairing, because Cormorant Garamond wants none at all.
Pitfalls
The @import in the CSS blocks rendering. It is there because a single
snippet that works when pasted into a stylesheet is more useful than one that
does not. For a site you care about, use the link tags output and put them in
<head> with the preconnects, or enqueue the fonts properly in WordPress.
display=swap is in the URL for a reason. Without it, the browser hides
text for up to three seconds waiting for the font. With it, the fallback shows
immediately and is replaced. That is the swap you see on a slow connection, and
it is the better of the two failures.
Only the weights in the snippet are loaded. Ask for font-weight: 500 on
something and you will get a synthesised weight rather than the real one. Add
the weight to the URL if you need it.
A monospace heading needs to be short. JetBrains Mono over six words stops reading as a headline and starts reading as a filename.
Fonts loaded from Google are a third-party request. In the EU that is a
consideration for a public site. Downloading the files and self-hosting them, or
using the WordPress font library, avoids it entirely; the CSS here works either
way once the @import is replaced with your own @font-face rules.
Compatibility
The CSS is plain custom properties and element selectors, so it works in any
theme, classic or block. The theme.json output is version 3, which WordPress
6.6 and later read; it declares the two families under
settings.typography.fontFamilies so they appear in the editor’s font picker,
and it turns fluid typography on.
theme.json on its own does not load the files. Either keep the link tags in
<head>, enqueue the Google URL, or add the families through the WordPress font
library, which downloads and self-hosts them.
Every family here is an open licence face on Google Fonts, so self-hosting is allowed.
Frequently asked questions
Why fourteen pairings and not a font picker?
Can I use one of these for both heading and body?
--font-heading and
--font-body to the same stack.Does the preview show what my theme will show?
Why is my body text not exactly the size I typed?
How do I self-host these instead?
@import with @font-face rules pointing at them. Keep the same
--font-heading and --font-body values and nothing else changes.