`font-display` for the Masses

Updated on January 12, 2017: Proper support checks are now part of the article body. Added information about the block value. Minor tweaks and copy edits. Enjoy!

If you're a regular reader here at CSS-Tricks, chances are good you know a bit about web fonts. You may even know a few useful tricks to control how fonts load, but have you used the CSS font-display property?

The font-display property in CSS is available in Chrome, and emerging in Firefox and Safari (but you might want to check browser support for yourself, since things change all the time). It's a simpler way of achieving what the Font Loading API is capable of, as well as third party scripts such as Bram Stein's Font Face Observer.

If all of this is new to you, no sweat. Let's first talk a bit about default font loading behavior in browsers.

Font Loading in the Browser

Browsers are carefully crafted programs, and they do a lot under the hood we may not suspect. Font loading is no exception to this rule. When a browser requests a font asset from a web server, any elements with styles invoking that font is hidden until the font asset has downloaded. This is known as the "Flash of Invisible Text," or FOIT.

The FOIT Effect in Action

This behavior is there to "save" us from seeing text initially render in a system font, only to swap to the custom typeface once it has loaded. That behavior is known as the Flash of Unstyled Text, or FOUT.

FOIT may sound preferable over FOUT, but FOIT has repercussions for users on slow connections. By default, most browsers will hide text for up to 3 seconds until displaying text in a fallback system typeface while waiting for a font to load. Other browsers like Safari will wait even longer, or perhaps indefinitely, leaving some users in a lurch with invisible text that may fail to render if a font request stalls.

We can solve this problem with a JavaScript-based solution to track when fonts load. We style the document to use system fonts by default, and then, when we detect that custom fonts have loaded, we add a CSS class to the document which in turn applies the custom typefaces to the document. This approach has been covered before. For example, assume you have a page that uses Open Sans Regular for all <p> elements. Initially, you'd use some CSS like this:

p {
  font-family: "Helvetica", "Arial", sans-serif;
}

As the font request is in flight, either Helvetica or Arial (depending on the fonts available on your system) will display first. When we've verified Open Sans Regular has loaded in JavaScript, we then apply a class of fonts-loaded to the <html> element, which will apply Open Sans to <p> elements via this next bit of CSS:

.fonts-loaded p {
  font-family: "Open Sans Regular";
}

These solutions work, but they can be rather unwieldy. That's where the font-display CSS property comes in.

Getting Acquainted with font-display

font-display is a CSS property available as of Chrome 61, Chrome for Android, Opera, and Safari's Technical Preview. Support for this property is incoming in Firefox 58. With font-display, we can control how fonts render in much the same way we can with JavaScript-based solutions, only now through a convenient CSS one-liner! font-display is used inside of a @font-face, and accepts the following values:

  • auto: The default. Typical browser font loading behavior will take place. This behavior may be FOIT, or FOIT with a relatively long invisibility period. This may change as browser vendors decide on better default behaviors.
  • swap: Fallback text is immediately rendered in the next available system typeface in the font stack until the custom font loads, in which case the new typeface will be swapped in. This is what we want for stuff like body copy, where we want users to be able to read content immediately.
  • block: Like FOIT, but the invisibility period persists indefinitely. Use this value any time blocking rendering of text for a potentially indefinite period of time would be preferable. It's not very often that block would be preferable over any other value.
  • fallback: A compromise between block and swap. There will be a very short period of time (100ms according to Google) that text styled with custom fonts will be invisible. Unstyled text will then appear if the custom font hasn't loaded before the short blocking period has elapsed. Once the font loads, the text is styled appropriately. This is great when FOUT is undesirable, but accessibility is more important.
  • optional: Operates like fallback in that the affected text will initially be invisible for a short period of time, and then transition to a fallback if font assets haven't completed loading. The similarities end there, though. The optional setting gives the browser freedom to decide whether or not a font should even be used, and this behavior hinges on the user's connection speed. On slow connections, you should anticipate custom fonts may possibly not load at all if this setting is used.

Now that we know the values font-display accepts, we can apply it to a @font-face rule. Here's an example of using the swap value in a @font-face for Open Sans Regular:

@font-face {
  font-family: "Open Sans Regular";
  font-weight: 400;
  font-style: normal;
  src: url("fonts/OpenSans-Regular-BasicLatin.woff2") format("woff2");
  font-display: swap;
}

Of course, we're abbreviating the @font-face a bit in this example by using only a WOFF2 file, but I'm going for brevity. In this example, we're using the swap option for font-display, which yields loading behavior like you see in the image below:

The font-display property's swap value in action.

When we control font loading in JavaScript, this often is the behavior we're after. We want to be sure the text is visible by default, then apply the custom typeface after it has downloaded.

But what constitutes "fallback text"? When you specify a value for a font-family property, you do so with a comma-separated list of font names. This is known as a font stack. The fallback is whatever system font is next in line after the primary (and presumably custom) typeface:

p {
  font-family: "Open Sans Regular", "Helvetica", "Arial", sans-serif;
}

In this example font stack, the custom font is Open Sans Regular. The system fonts are Helvetica and Arial. When font-display: swap; is used, the initial font displayed is the first system font in the stack. When the custom font has loaded, it will kick in and replace the system font that was initially displayed. Using font-display values of fallback and optional will also rely on system fonts in the stack when necessary.

Most of the time you'll use swap.

If you don't know which option to use, go with swap. It allows you to use custom fonts and tip your hand to accessibility. If you use fonts that are "nice to have", but could ultimately do without, consider specifying optional.

What if font-display isn't supported?

As it goes with newer browser features, support for font-display isn't ubiquitous. Therefore, you have two choices concerning its use:

  1. You can just use font-display and that's that. If other browsers don't support it, they will behave according to their defaults. This isn't necessarily bad in that it doesn't break anything, but it may not be optimal for you.
  2. You can detect support for font-display and provide an alternative. Consider doing this if time and resources allow.

If you decide to go with option 2, you'll need to detect support for font-display. This is possible as demonstrated in this fiddle (discovered from this Github discussion):

try {
  var e = document.createElement("style");
  e.textContent = "@font-face { font-display: swap; }";
  document.documentElement.appendChild(e);
  var isFontDisplaySupported = e.sheet.cssRules[0].cssText.indexOf("font-display") != -1;
  e.remove();
} catch (e) {
  // Do something with an error if you want
}

From here, we can decide what to do based on the value of the isFontDisplaySupported variable. One way you might use this feature check would be to fall back to the Font Loading API in the absence of font-display like so:

if (isFontDisplaySupported === false && "fonts" in document) {
  document.fonts.load("1em Open Sans Regular");
  document.fonts.ready.then(function(fontFaceSet) {
    document.documentElement.className += " fonts-loaded";
  });
}
else {
  // Maybe figure out your own strategy, but this might be sensible:
  document.documentElement.className += " fonts-loaded";
}

In this example, the Font Loading API handles the transition if isFontDisplaySupported is false. Once the API knows a font has loaded, we can apply a class of fonts-loaded to the <html> tag. With this class applied, we can then write CSS that allows a progressive application of custom typefaces:

p {
  font-family: "Helvetica", "Arial", sans-serif;
}

.fonts-loaded p {
  font-family: "Open Sans Regular";
}

Obviously, we'd prefer to use a CSS one-liner like font-display to do all of this stuff for us, but at least we have the ability to fall back to another solution if necessary. As time marches on, font-display (as well as the Font Loading API) may be implemented in most (if not all) browsers.

What About Third Party Font Providers?

If you embed fonts using a third party service like Google Fonts or TypeKit, there's not much you can do at the moment. Third party services control the content of a @font-face they host, so perhaps consider hosting your own fonts (which I talk about in this article).

As time goes on, providers may make font-display a configurable option when you retrieve an embed code from their service. font-display is being actively discussed for potential implementation in Google Fonts, but don't assume every third party service will necessarily scramble to implement it.

Either way, font-display is a welcome addition to the web typography landscape. It greatly simplifies what is otherwise an unwieldy sort of task in JavaScript. Try out font-display for yourself, and see what it can do for you and your users!


Cover of Web Performance in Action

Jeremy Wagner is the author of Web Performance in Action from Manning Publications. Use coupon code sswagner to save 42%!

Check him out on Twitter: @malchata