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.
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.
font-display
Getting Acquainted with 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 beswap
ped 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 thatblock
would be preferable over any other value.fallback
: A compromise betweenblock
andswap
. 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 likefallback
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. Theoptional
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:
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.
swap
.
Most of the time you’ll use 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
.
font-display
isn’t supported?
What if As it goes with newer browser features, support for font-display
isn’t ubiquitous. Therefore, you have two choices concerning its use:
- 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. - 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.
Update:
Google Fonts does swap
by default now. Horray!
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!
Wouldn’t adding the JS condition to test that this is supported cancel out the benefit of using this?
I’m not entirely certain what you mean– Do you mean in light of the fact that my feature detection code written in the article wasn’t 100% reliable? Then yeah, in some situations it may. However, if the feature’s support is detected properly and reliably, then any JavaScript fallback code would only execute in the instance that
font-display
is unavailable.You can use Feature query for this https://hacks.mozilla.org/2016/08/using-feature-queries-in-css/
I’ve tried, but to no avail: https://css-tricks.com/font-display-masses/#comment-1603606
Could you use @supports for the feature detection rather than js? (and fallback for everyone else)?
Yes! You certainly could. My only hesitation with doing it this way is that feature queries and the @supports JS API is not universally, er, supported. In which case, I would prefer a single more widely supported method that determines support for
font-display
(or any feature, really.)Are there any browsers that support font-display but not @supports though?
Good point, actually. Although when I check in JavaScript in Chrome Canary with this code…
…it’s less clear to me if supports works as intended. I get the same sort of result when checking for
unicode-range
support usingCSS.supports
as well:So I’m not immediately certain how one would check for
@font-face
-specific rules withCSS.supports
. I’m relatively new to using the supports API. Using the feature query syntax yields similar results:If anyone has any ideas, I’d love to hear them, because this would be a much simpler way to check for any CSS feature support, let alone
font-display
.Unless I’m missing something (which I probably am) its not supported in canary (theres no syntax highlighting for font-display when editing the css file in sources) and I couldn’t find any reference to it in flags? Likewise for Opera. Although its suggested both should work? https://developers.google.com/web/updates/2016/02/font-display?hl=en
Trying to test with @supports (css) or CSS.supports (js) I’m not getting anywhere closer than you are, with all 3 browsers reporting as false. Very confusing.
Brendan
Hi All,
It can’t work with CSS supports.
I raised a bug-report here and turned out to be invalid as Chromium developers explain here:
https://bugs.chromium.org/p/chromium/issues/detail?id=600137
“This is actually working as intended. font-display is a descriptor, not a property, and CSS.supports is only for properties (there is both a font-family descriptor and font-family property so it works in CSS.supports).”
I suppose that makes sense, interesting.
Hi Jeremy,
That font-display feature detection may not work. It works in Chrome (when enabled) but it shouldn’t be.
It doesn’t work in Firefox (when enabled), as it should be.
Just today a Mozilla/Firefox developer told me this is the way to feature detect:
https://jsfiddle.net/p7g8y7du/
The background is here:
https://bugzilla.mozilla.org/show_bug.cgi?id=1296373
Thank you for this! My feature detection technique is apparently not bulletproof. :)
Chris has updated the article to point to a Github discussion where you pointed this out. I don’t have the ability to update the article, so it’s at Chris’ discretion to have me update the code in the article (which I would, if he so asked.)
Thanks again!
I would rather see FOIT than FOCF – flashes of changing font IMO it’s more disturbing to see text change font than it is to see text go from invisible to visible
Indeed, a few years ago, we were working hard to figure out a way around FOUT (flash of unstyled text), which was exactly the situation created by the
swap
value here. I guess everything old really is new again.Yes, of course, if you are on a fast connection. But if you’re not, it’s better to see unstyled text for a few seconds (or more) than to see nothing at all. With FOUT the transition is „uglier“, but the website is usable sooner.
Thanks for the article, I wasn’t aware of this possibility and I already wanted something like this to avoid transparent text.
On the problem with external providers, maybe the real answer is—avoid using them? I always prefer to copy the fonts and CSS locally, avoiding any request to an external provider like Google Fonts, so a part of the code is not dependant on an external website able to change it without control.
I can definitely understand why some may want to avoid external providers, but those providers have some seriously deep pockets to devote to distributing resources globally for faster delivery. I think any third party provider worth their salt will eventually allow this to be configurable.
That said, you could always do what you’ve said you already do, but put a CDN service in front of your site like Cloudflare, which will achieve a similar effect. Hmn. Maybe I need to do this for my own site…
Thanks for reading!
How did go from “OMG, FOUT = badness!” to it being the desired effect?
Accessibility. When browsers block the rendering of text for an unacceptably long amount of time, the possibility of permanently hidden text is an unacceptable outcome for some.
Fair enough, it’s just funny the back-and-forth, though for sure.
As someone said in another comment, everything old is new again, but this time it’s for a good reason. :) Thanks for reading, man.
Hi Jeremy,
Thanks for sharing this article, i hope the browsers will provide a support for this soon!
Why the browser doesn’t fallback to the next entry on the
font-family
list? It seems the most obvious way to manage the problem.Example:
I set
font-family: "Asap", Arial;
I load the page, the webfont is still loading, so
Asap
is still not available, the browser should fallback to Arial, and once the webfont is loaded (available) it should switch to it.Doesn’t it makes sense?
I’m curious why aren’t you using classlist API to manage CSS classes? You’re concatenating strings …
If all I’m doing is adding a class to the HTML element, I can achieve the broadest possible coverage by just concatenating the
fonts-loaded
class (classList
has about 90% support in browsers globally). If I need stuff like toggling/removing, I’ll useclassList
in that case. I fully admit, it’s kind of a persnickety way to do it.What About 3rd Bash Font Companies?
If you embed fonts working with a 3rd get together support like Google Fonts or TypeKit, you can find not much you can do. The font-exhibit home need to be utilized inside of of a @font-deal with declaration. Simply because you don’t regulate the CSS that the font service provider serves, you are unable to regulate the existence of the font-exhibit home in that instance, much fewer the value that is handed to it. http://www.iadmission.org/
So no one has come up with a solution that can fix both FOIT AND FOUT yet?