Perfect Font Fallbacks

Thankfully, we have a lot more control over font loading these days. We no longer have to force our users to wait for a font to load before showing them text, we can swap it out as the font loads. But that swap can be visually rough with lots of abrupt shifts and reflow. We can fix that though!

When you load custom fonts on the web, a responsible way to do that is to make sure the @font-face declaration uses the property font-display set to a value like swap or optional.

@font-face {
  font-family: 'MyWebFont'; /* Define the custom font name */
  src:  url('myfont.woff2') format('woff2'); /* Define where the font can be downloaded */
  font-display: swap; /* Define how the browser behaves during download */

With that in place, there will be no delay for a user loading your page before they see text. That’s great for performance. But it comes with a design tradeoff, the user will see FOUT or “Flash of Unstyled Text”. Meaning they’ll see the page load with one font, the font will load, then the page will flip out that font for the new one, causing a bit of visual disruption and likely a bit of reflow.

This trick is about minimizing that disruption and reflow!

This trick comes by way of Glen Maddern who published a screencast about this at Front End Center who uses Monica Dinculescu’s Font style matcher combined with Bram Stein’s Font Face Observer library.

Let’s say you load up a font from Google Fonts. Here I’ll use Rubik in two weights:

@import url(";900&display=swap");

At the end of that URL, by default, you’ll see &display=swap which is how they make sure font-display: swap; is in the @font-face declaration.

On a slow connection, this is how a simple page with text will load:

First you’ll see the fallback typography, then the custom fonts will load and you’ll see the typography swap to use those.

See the swap? Remember that’s good, in a way, because at least the text is visible to begin with. But the swap is pretty heavy-handed. It feels visually disruptive.

Let’s fix that.

Using Font style matcher tool, we can lay the two fonts on top of each other and see how different Rubik and the fallback font are.

Note I’m using system-ui as the fallback font here. You’ll want to use a classic “web-safe” font for a fallback, like Georgia, Times New Roman, Arial, Tahoma, Verdana, etc. The vast majority of computers have those installed by default so they are safe fallbacks.

In our case, these two fonts have a pretty much identical “x-height” already (note the height of the red and black lowercase letters above). If they didn’t, we’d end up having to tweak the font-size and line-height to match. But thankfully for us, just a tweak to letter-spacing will get them very close.

Adjusting the callback to using letter-spacing: 0.55px; gets them sizing very close!

Now the trick is to give ourselves the ability apply this styling only before the font loads. So let’s make it the default style, then have a body class that tells us the font is loaded and remove the alterations:

body {
  font-family: "Rubik", system-ui, sans-serif;
  letter-spacing: 0.55px;
body.font-loaded {
  letter-spacing: 0px;

But how do you get that font-loaded class? The Font Face Observer library makes it very easy and cross-browser friendly. With that library in place, it’s a few lines of JavaScript to adjust the class:

const font = new FontFaceObserver("Rubik", {
  weight: 400

font.load().then(function() {

Now see how much smoother and less disruptive the font loading experience is:

That’s a really great trick!

Here’s that minimal demo:

When testing, if you can’t see the swap happen at all, check and make sure you don’t have Rubik installed on your machine already. Or your internet might be just too fast! DevTools can help throttle your connection to be slower for testing:

This can get more intricate as you use multiple fonts and multiple weights of fonts. You can watch for the loading of each one and adjust different classes as they load in, adjusting styles to make sure things reflow as little as possible.