Two typefaces, and why you probably want one
The default is one
One family with a full range of weights will carry almost any project. Regular for body, semibold for subheadings, bold for the page title, light for a large display line, italic for emphasis. That is five distinct typographic treatments from one file set, all guaranteed to sit together because one designer drew them.
A second typeface is a decision that needs a reason. Good reasons exist — a serif for long reading against a sans for interface labels, a monospace for code, a distinctive display face for a masthead that would be tiring in a paragraph. A vague sense that one font is boring is not a reason; it is usually a symptom of a hierarchy problem, and adding a face will not fix it.
If you do pair, pair across a class boundary
The mechanism is the threshold rule again. Two faces that are slightly different read as a mistake; two that are clearly different read as a decision.
So pair across categories — a serif with a sans, a sans with a monospace, a geometric sans with a humanist serif. Do not pair two humanist sans faces, or two transitional serifs. The reader cannot tell whether the difference is intentional, and the pair will look like a font failed to load somewhere.
What to match when you pair:
- x-height. Two faces with wildly different x-heights at the same point size will look like different sizes. Adjust the point size of one until the lowercase letters are the same height.
- Apparent weight. A regular in one face may be visibly heavier than a regular in another. Compare the two at the same size and adjust the weight rather than assuming the names mean the same thing.
- Width and rhythm. A condensed face beside a wide one produces a texture difference that competes with the hierarchy.
Superfamilies remove the problem
Several families ship multiple classes designed to work together, with shared metrics and shared skeletons. This is the lowest-risk pairing available and all of these are free:
- IBM Plex — Sans, Serif, Mono, Condensed, plus coverage for several Indic scripts.
- Source — Source Sans, Source Serif, Source Code, from Adobe under the OFL.
- Noto — Sans and Serif across an extraordinary range of scripts, which matters if your content is multilingual.
- Roboto — Sans, Slab, Mono, Condensed.
- Fira — Sans, Sans Condensed, Code.
Using a superfamily is not a lesser choice. It is the choice a lot of professional work makes, because the compatibility is guaranteed rather than eyeballed.
The test
Set the same short phrase in both faces, at matched x-height, one directly above the other, at the size each will actually be used. Then look at it small, and then blurred.
Three outcomes:
- Clearly different, and comfortable together. A pairing.
- Nearly the same. Not a pairing. Drop one.
- Fighting. Usually a mismatch of apparent weight or width rather than of class. Adjust before abandoning.
This takes about two minutes and replaces a large amount of published advice about which fonts go with which, most of which is one person's taste presented as a rule.
Assign jobs, then hold the line
The part that fails in practice is not the choice of faces but the discipline afterwards. Write down which face does what:
Display face → page titles only, 40px and above
Text face → everything else, including all headings below 40px
Mono → codes, reference numbers, tabular dataWithout that, the display face migrates into subheadings, then into buttons, and after three months the distinction has dissolved and both faces are doing everything. The pairing has stopped carrying information, which was the only thing it was for.
The honest note on cost
Each additional family costs bytes. A single weight of a full-coverage web font is commonly 15 to 40 kilobytes in WOFF2; a family with four weights and matching italics is easily 200 kilobytes, and with Devanagari or CJK coverage it can be far larger. On a slow connection that is a visible delay before the text appears, or a visible jump when it swaps in.
Subsetting to the characters and scripts you actually use, and serving with font-display: swap, mitigates it. The real mitigation is using fewer families, which is where this lesson started.
The one thing to keep
One family with many weights is the default and a second typeface needs a stated reason; when you do pair, cross a class boundary, match x-height and apparent weight, and write down which face does which job.
Before you move on
A team pairs Open Sans with Lato and reviewers keep saying the headings look like a rendering bug rather than a design choice. What is the underlying issue?
Pick the one you would defend. Nobody sees your answer.