A scattered set of wooden typesetting letters and symbols.

The perfect cross-platform serif and sans-serif font stacks

Different web browsers use the same default fonts on the same operating system. However, the default fonts differ between operating systems and few fonts are available everywhere. In this article, I’ll focus on making the default fonts look and behave the same across operating systems.

The default fonts for the generic “serif” and “sans-serif” (without serifs) fonts are metrically compatible across MacOS, iOS, Windows, and Chrome OS. There are good options available for Linux too, but those are rarely set as the default fonts. Android only has one font, so you get what you get.

Metrically compatible means that each individual character in one font has the same width as the same characters in another font. Metrical compatibility means you can substitute one font for another in a document without affecting its layout. For example, a headline and paragraph should fill the same number of lines of text. This isn’t the same as a fixed-width font (monospace), where all the characters in the font have the same width.

We can thank the PostScript printing standard for defining a core set of fonts that everyone had to support. Not every platform includes the exact fonts specified in the standard; because of copyright issues and licensing costs. However, it’s still possible to use these fonts across platforms.

CSS lets you list multiple fonts in an ordered “font stack.” Web browsers will choose the first installed font they recognize from the list. Let’s create a font stack that will look and behave nearly identical across platforms. So, what fonts do we have to work with?

PostScript version 1 required one serif font, Times; and one sans-serif font, Helvetica. These fonts should be available on every operating system. The following table shows the default fonts on most operating systems except Linux. I’ll get back to Linux a little later in the article.

Default fonts (Latin scripts)
Font name serif sans-serif
Windows Times New Roman Arial
Chrome OS Tinos Arimo
MacOS Times Helvetica
iOS Times New Roman Helvetica

Tinos and Times New Roman, as the name suggests, are newer versions of the classic Times font. They improve character legibility over the original and tone down some serif-ornamentation. Arial is a re-interpretation of the classical Helvetica font (under a cheaper license). MacOS also ships with both Arial and Times New Roman.

If you’re willing to sacrifice a little metrical compatibility, you can upgrade the default sans-serif fonts to more modern versions. Mac and iOS also come with Helvetica Neue preinstalled. It addresses some of Helvetica’s legibility issues, e.g. by elongating crossbars and making punctuation marks heavier. Upgrading to Helvetica Neue is probably a better option overall than switching to Arial.

On Windows, you might find a more modern version of Arial called Arial Nova. Arial Nova makes much the same legibility improvements and compromises as Helvetica Neue. It isn’t pre-installed in Windows but is offered for free in the Pan-European Supplemental Fonts package (an optional feature) for Windows and the Microsoft Store app.

You can think of using these fonts as a progressive enhancement to the classics fonts. They improve the reading experience when available, but it’s not a great loss if they’re not. Your layouts may shift ever-so-slightly when using these fonts compared to the originals. You can exclude them from your font stacks if this is a showstopper.

Let’s recap everything I’ve talked about so far into a CSS example:

.headlines, .ui {
  font-family: 'Helvetica Neue',
               'Arial Nova',
                sans-serif;
}
.body-text {
  font-family: serif;
}

Note that I didn’t specify either Helvetica or Arial, as these are already the default fonts in the sans-serif group. You’ll get these fonts when the newer fonts aren’t available. This assumes that the document is written in a Latin script and is properly labeled as such.

Make sure web browsers know what language your pages are written in when using generic font names! Always specify the document language, e.g. using <html lang="en">. All bets are off when it comes to the default fonts for other writing systems.

If you can’t control the document language, then you need to include the names of the desired fonts in your font stack instead. The order matters, so include the default after the progressive enhancement fonts. To discuss another aspect of how operating systems handle fonts; here is the same font stack as above, but including the default fonts.

.headlines, .ui {
  font-family: 'Helvetica Neue',
               'Arial Nova',
                Helvetica,
                Arial,
                sans-serif;
}
.body-text {
  font-family: Times,
              'Times New Roman',
               serif;
}

Note that I listed Times in front of Times New Roman. Times New Roman is arguably better than Times. However, the version of Times New Roman that is distributed with MacOS lacks features like common glyph ligatures. Times provides a better experience on Mac. I’ll get back to this later, but Times also yields a better result on Linux.

I still included the generic font names for platforms where these fonts are unavailable. Android only comes preinstalled with one or two system font families. Roboto is a sans-serif included since Android version 4 (released in 2013). It’s still the default sans-serif font today, but on newer devices, you might also find the Noto Serif font. You can at least decide between a serif or sans-serif font. The two fonts are metrically compatible with each other. Roboto is also a near-match to Helvetica.

In the above example, the complexity of this topic skyrockets because of font name substitutions. Windows will recognize the Times and Helvetica font names from the PostScript standard, and substitutes them for Times New Roman and Arial. You can get away with just specifying just Times and Helvetica. (Technically, you don’t need either because of the generic font name — but you get what I mean!)

Chrome OS applies the same trick with the Times New Roman and Helvetica font names. As shown in the above table, iOS also substitutes the Times font for Times New Roman.

You should now know enough to delve into the default fonts on Linux! Linux comes in many flavors, and different distributions come with different default and pre-installed fonts. They’ve all got one thing in common, though: they rely on FontConfig to handle configure their fonts. (Not coincidentally, so does Chrome OS.)

FontConfig has a font name substitution system similar to the one found in Windows. It’s time for two more tables to cover serif and sans-serif font name substitutions.

Serif font name substitutions (Latin scripts)
Distribution serif Times Times New Roman
Raspberry Pi OS 21.01 Liberation Serif Liberation Serif Liberation Serif
Ubuntu / Mint / Pop!_OS 21.04 DejaVu Serif Nimbus Roman Liberation Serif
Fedora Linux 34 DejaVu Serif Nimbus Roman Liberation Serif
ElementaryOS 5.1 DejaVu Serif* Tinos Tinos
Solus 4.2 Clear Sans† Nimbus Roman‡ Liberation Serif‡
EndeavorOS 21.04 DejaVu Serif Nimbus Roman‡ Liberation Serif
openSUSE 15.3 DejaVu Serif Nimbus Roman‡ Liberation Serif‡
  • ElementaryOS intends to use Tinos for its generic name. It’s a bug that it isn’t working. I’ve sent the project a patch.
  • This is a sans-serif font!
  • Not installed by default. You get the generic serif font instead.
Sans-serif font name substitutions (Latin scripts)
Distribution sans-serif Helvetica Arial
Raspberry Pi OS 21.01 Liberation Sans Liberation Sans Liberation Sans
Ubuntu / Mint / Pop!_OS 21.04 DejaVu Sans Nimbus Sans Liberation Sans
Fedora Linux 34 DejaVu Sans Nimbus Sans Liberation Sans
ElementaryOS 5.1 Arimo Arimo Arimo
Solus 4.2 Clear Sans Nimbus Sans‡ Liberation Sans‡
EndeavorOS 21.04 DejaVu Sans Nimbus Roman‡ Liberation Sans
openSUSE 15.3 Cantarell Nimbus Sans‡ Liberation Sans‡
  • Not installed by default. You get the generic serif font instead.

The DejaVu font family isn’t metrically compatible with neither Times nor Helvetica. Fonts like DejaVu Sans and Cantarell more closely resemble the Verdana font. Verdana is a heavier and wider font created for the ultra-low screen resolutions of decades past. Clear Sans closely resembles Arial, but it’s slightly wider and has a lower x-height. It’s a close match, but not metrically compatible.

In summary: the default generic font names on Linux don’t resolve fonts that are metrically compatible with the PostScript fonts. This is why the text on webpages often looks quite different on Linux compared to MacOS and Windows. Raspberry Pi OS and ElementaryOS being the partial exceptions.

You can rely on the generic font names alone like you can on other operating systems. The named font name substitution fonts are, however, a better option. The Nimbus font family provides a Roman variant that is metrically compatible with Times. There’s also a Sans variant drop-in replacement for Helvetica. The same goes for the Liberation font family’s Serif variant for Times New Roman, and a Sans for Helvetica. While the font name substitution is mostly functional on all Linux distributions, the fonts that get swapped in aren’t necessarily installed by default.

There are tons of problems with the font name substitution configuration all across the board on Linux. Solus straight up returns a sans-serif font instead of a serif font. It does the name substitutions but then doesn’t come preinstalled with those fonts. Instead, you get the generic serif font (which is configured to be a sans-serif font!) Users can install the fonts to fix the problem, but they’re likely unaware that they’re missing.

Raspberry Pi OS and ElementaryOS include an older version of the Nimbus font family under slightly different names. Those older names aren’t recognized by the font name substitution system. The Nimbus font family was re-engineered and renamed around 2015. The old fonts were named Nimbus Sans L and Nimbus Roman No9 L. You could include these in your font stack for increased compatibility with older systems. However, the older fonts weren’t as high quality as their more recent counterparts. Specifically, kerning was sub-par.

The modern version of Nimbus Roman is a better substitute for Times New Roman. It’s visually a much closer match than Liberation Serif. You should include the font name Nimbus Roman before Times New Roman in your font stack. You can skip this if you already list Times before Times New Roman (as recommended in this article).

It seems that including the default font names in our font stack is the right way to go. More common font names in your font stack give the font name substitution systems more opportunities to save the day. It also sweeps the internationalization issues discussed above under the rug.

In summary, neither MacOS, iOS, Chrome OS, nor Windows need you to specify the default serif and sans-serif font names. The generic font names are enough. (As long as you specify the document language!) Including them results in better font selection on Linux; even without naming a single font that comes preinstalled with it. You can sacrifice a tiny amount of font metrics stability for improved legibility by using newer versions of fonts.

There you have it: the perfectly safe, boring, and default cross-platform serif and sans-serif font stacks. (I had to abbreviate its full name in the title due to space constraints.)

Thanks for reading. I hope you learned something new! Follow the blog so you won’t miss the next parts of this mini-series! Up next, I’ll discuss: the cross-platform monospace and coding font stacks.