99

Google's Web Fonts API offers a way to define callback functions to be executed if a font has finished loading, or couldn't be loaded etc. Is there a way to achieve something similar using CSS3 web fonts (@font-face)?

Daniel Wagner
  • 2,717
  • 1
  • 21
  • 14
  • see also http://stackoverflow.com/questions/11941883/call-jquery-function-after-fonts-are-loaded for approaches using Google Fonts API – Simon_Weaver Mar 20 '13 at 01:54

6 Answers6

166

2015 Update

Chrome 35+ and Firefox 41+ implement the CSS font loading API (MDN, W3C). Call document.fonts to get a FontFaceSet object, which has a few useful APIs for detecting the load status of fonts:

  • check(fontSpec) - returns whether all fonts in the given font list have been loaded and are available. The fontSpec uses the CSS shorthand syntax for fonts.
    Example: document.fonts.check('bold 16px Roboto'); // true or false
  • document.fonts.ready - returns a Promise indicating that font loading and layout operations are done.
    Example: document.fonts.ready.then(function () { /*... all fonts loaded...*/ });

Here's a snippet showing these APIs, plus document.fonts.onloadingdone, which offers extra information about the font faces.

alert('Roboto loaded? ' + document.fonts.check('1em Roboto'));  // false

document.fonts.ready.then(function () {
  alert('All fonts in use by visible text have loaded.');
   alert('Roboto loaded? ' + document.fonts.check('1em Roboto'));  // true
});

document.fonts.onloadingdone = function (fontFaceSetEvent) {
   alert('onloadingdone we have ' + fontFaceSetEvent.fontfaces.length + ' font faces loaded');
};
<link href='https://fonts.googleapis.com/css?family=Roboto:400,700' rel='stylesheet' type='text/css'>
<p style="font-family: Roboto">
  We need some text using the font, for the font to be loaded.
  So far one font face was loaded.
  Let's add some <strong>strong</strong> text to trigger loading the second one,
    with weight: 700.
</p>

IE 11 doesn't support the API. Look at available polyfills or support libraries if you need to support IE:

Community
  • 1
  • 1
Dan Dascalescu
  • 143,271
  • 52
  • 317
  • 404
  • 2
    In Firefox 44 on Linux it looks like `check` always returns `true`: `document.fonts.check('1em NoThisReallyDoesntExist')`. Looking at [the MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/CSS_Font_Loading_API) it looks like this is very much an experimental/incomplete feature. It does seem to work correctly in Chromium. – Martin Tournoij Feb 09 '16 at 13:03
  • Do you perhaps know how to elegantly hook up to this in Angular2? I wish there was a `ngAfterFontInit` in ng2 interface but there's not... – user776686 Nov 07 '16 at 12:48
  • 1
    only way i could get this working is to use `onloadingdone`. `.ready` always fires immediately – Jason Apr 21 '17 at 23:35
  • [Can I use - CSS Font Loading](https://caniuse.com/#feat=font-loading). – Alvaro Aug 29 '17 at 10:30
  • 1
    Seems to me that just including the font-face does not necessary mean loading will start. You need to use the font (e.g. in the html/CSS or in the JS-code) Only then did I get the fonts loaded. I.e. just wiating for font loading in JS when u want to use it in js does not suffice – TomFree Feb 15 '21 at 11:02
  • .onloadingdone and .ready seem to fire after the fonts are loaded but before they're rendered. – W Biggs Mar 16 '21 at 20:24
  • Does not work if webfont loaded from a .css? – Extrange planet Jun 25 '23 at 16:41
20

Tested in Safari, Chrome, Firefox, Opera, IE7, IE8, IE9:

function waitForWebfonts(fonts, callback) {
    var loadedFonts = 0;
    for(var i = 0, l = fonts.length; i < l; ++i) {
        (function(font) {
            var node = document.createElement('span');
            // Characters that vary significantly among different fonts
            node.innerHTML = 'giItT1WQy@!-/#';
            // Visible - so we can measure it - but not on the screen
            node.style.position      = 'absolute';
            node.style.left          = '-10000px';
            node.style.top           = '-10000px';
            // Large font size makes even subtle changes obvious
            node.style.fontSize      = '300px';
            // Reset any font properties
            node.style.fontFamily    = 'sans-serif';
            node.style.fontVariant   = 'normal';
            node.style.fontStyle     = 'normal';
            node.style.fontWeight    = 'normal';
            node.style.letterSpacing = '0';
            document.body.appendChild(node);

            // Remember width with no applied web font
            var width = node.offsetWidth;

            node.style.fontFamily = font;

            var interval;
            function checkFont() {
                // Compare current width with original width
                if(node && node.offsetWidth != width) {
                    ++loadedFonts;
                    node.parentNode.removeChild(node);
                    node = null;
                }

                // If all fonts have been loaded
                if(loadedFonts >= fonts.length) {
                    if(interval) {
                        clearInterval(interval);
                    }
                    if(loadedFonts == fonts.length) {
                        callback();
                        return true;
                    }
                }
            };

            if(!checkFont()) {
                interval = setInterval(checkFont, 50);
            }
        })(fonts[i]);
    }
};

Use it like:

waitForWebfonts(['MyFont1', 'MyFont2'], function() {
    // Will be called as soon as ALL specified fonts are available
});
Thomas Bachem
  • 1,545
  • 1
  • 16
  • 10
  • 2
    You can set it to visible:hidden and it will still be measurable – izb May 16 '13 at 14:25
  • 2
    The callback() function gets called for every font that's loaded, I guess it should only be called once? – Lutsen May 27 '13 at 10:15
  • Should be noted, that one needs extra quotes if the font-family's name requires so: `waitForWebfonts(['"Vectora W01 56 Italic"'],...);` – Lukx Dec 04 '13 at 12:03
  • Never gets called in Chrome Version 59.0.3071.86 – Chewie The Chorkie Jun 14 '17 at 17:07
  • 1
    Does not work with Chrome Version 73.0.3683.86 - Function gets triggered before the font has loaded. Tested with network throttling "Slow 3G" preset and Chrome only, didn't check other browsers. – Norman Apr 08 '19 at 08:49
  • 1
    There's one problem with this approach (explains @Norman's issue), otherwise it's awesome! You originally set the font face to `sans-serif`, but if you request a `serif` font: `"Roboto Slab", serif` the browser will naturally switch to a default `serif` font while waiting for Roboto Slab to load, triggering the measurement prematurely. Best way to fix this imo is to figure out the requested font's fallback family and use that when creating the node so that the only swap would happen once the final font has loaded. – Tony Bogdanov Jan 04 '20 at 11:55
  • @TonyBogdanov You mean when using it like `waitForWebfonts(['"Roboto Slab", serif'], function() { ... });`? Specifying multiple fonts per string (also fallback fonts) was never intended. – Thomas Bachem Jan 04 '20 at 16:06
  • @ThomasBachem Not specifying a fallback font is not an option, if you don't specify the font like this: `Roboto, sans-serif`, but use `Roboto` instead, the browser will still add `sans-serif` internally. This is even worse, because if you use, say a `monospace` type of font and not tell the browser that it's monospace, the browser will assume `sans-serif` (for example) until it downloads the font. You'll face the same issue. – Tony Bogdanov Jan 05 '20 at 17:04
  • This will only work if default font family is `sans-serif` which isn't always a case. Instead I'm checking if the font is loaded with `document.fonts.check` and it works perfect in all scenarios. Replace `if(node && node.offsetWidth != width) {` with `if(node && document.fonts.check('16px fontName')) {` – Nika Tsogiaidze Dec 15 '22 at 06:26
12

The JS Library used by Google Web Fonts API (and Typekit) can be used without the service: WebFont Loader.

It defines callbacks for what you ask, and many more.

bpierre
  • 10,957
  • 2
  • 26
  • 27
  • 1
    It's not obvious from the Google site, but the library is available for download: https://github.com/typekit/webfontloader – z0r Mar 25 '13 at 06:16
10

2017 Update

The JS library FontFaceObserver is definitely the best, most lightweight, cross-browser solution as of 2017. It also exposes a Promise-based .load() interface.

fisch2
  • 2,574
  • 2
  • 26
  • 29
  • this lib has some issues with icon fonts https://github.com/bramstein/fontfaceobserver/issues/109#issuecomment-333356795 – maersu Feb 28 '19 at 09:10
  • 1
    @maersu that one is easy: according to [what this guy says](https://github.com/bramstein/fontfaceobserver/issues/109#issuecomment-333356795) just choose non-square character from the font and `var observe_fa = new FontFaceObserver('FontAwesome'); observe_fa.load('').then(...)`. – vintprox Feb 28 '19 at 12:39
  • @vintproykt unless the font has only square characters ;) at least I did not find any non-square in Google Material Icons. – maersu Mar 01 '19 at 13:07
0

I created two methods to check for a specific font. The first method is the best one as it uses the 'fonts' interface directly with the 'check' method. The second method is not as good, but still functional, as it detects the difference directly in the DOM by comparing the size of the text with the default font to the text with the new font. It's possible, although rare, for the fonts to be so close in size that the event wouldn't fire, but I think it's highly unlikely. If that happens, you can add another span to check the difference between the serif font as well.

(Although it is pure javascript it works with React)

METHOD 1

const fontName = "Fira Sans Condensed",
    maxTime = 2500 // 2.5s

// EXAMPLE 1
fontOnload(fontName).then(() => {
    console.log("success")
})

// EXAMPLE 2
fontOnload(fontName, maxTime).then(() => {
    console.log("success")
}).catch(() => {
    console.log("timeout")
})

async function fontOnload(fontName, maxTime = Infinity, timeInterval = 10) {
    const startTime = performance.now()

    return new Promise((resolve, reject) => {
        setInterval(() => {
            const currentTime = performance.now(),
                elapsedTime = currentTime - startTime
            if (document.fonts.check("12px " + fontName)) {
                resolve(true)
            } else if (elapsedTime >= maxTime) {
                reject(false)
            }
        }, timeInterval)
    })
}

METHOD 2

const fontName = "Fira Sans Condensed",
    maxTime = 2500 // 2.5s

// EXAMPLE 1
fontOnloadDOM(fontName).then(() => {
    console.log("success")
})

// EXAMPLE 2
fontOnloadDOM(fontName, maxTime).then(() => {
    console.log("success")
}).catch(() => {
    console.log("timeout")
})

async function fontOnloadDOM(fontName, maxTime = Infinity, timeInterval = 10) {
    return new Promise((resolve, reject) => {
        const startTime = performance.now(),
            abc = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
            mainStyle = "font-size:24px!important;display:inline!important;font-family:",
            body = document.body,
            container = document.createElement("div"),
            span1 = document.createElement("span"),
            span2 = document.createElement("span")

        container.classList.add("font-on-load")
        container.setAttribute("style", "display:block!important;position:absolute!important;top:-9999px!important;left:-9999px!important;opacity:0!important;")

        span1.setAttribute("style", mainStyle + "sans-serif!important;")
        span2.setAttribute("style", mainStyle + "\"" + fontName + "\",sans-serif!important;")

        span1.innerText = abc.repeat(3)
        span2.innerText = abc.repeat(3)

        container.append(span1, span2)
        body.append(container)

        const interval = setInterval(() => {
            const currentTime = performance.now(),
                elapsedTime = currentTime - startTime,
                width1 = span1.clientWidth || span1.getBoundingClientRect().width,
                width2 = span1.clientWidth || span2.getBoundingClientRect().width,
                diffWidths = Math.abs(width1 - width2)

            if (diffWidths > 9) {
                clearInterval(interval)
                resolve(true)
            } else if (elapsedTime >= maxTime) {
                clearInterval(interval)
                reject(false)
            }
        }, timeInterval)
    })
}
Luis Lobo
  • 489
  • 4
  • 7
-6

The window.load event will fire when everything has loaded - that should include fonts So you could use that as the call back. However I don't think you have to is you decide to use the web font loader as

In addition to the google, typekit, ascender and monotype options, there is also a custom module that can load a stylesheet from any web-font provider.

WebFontConfig = { custom: { families: ['OneFont', 'AnotherFont'], urls: [ 'http://myotherwebfontprovider.com/stylesheet1.css', 'http://yetanotherwebfontprovider.com/stylesheet2.css' ] } };

The library sends the same events regardless of which provider you specify.

Shaun Hare
  • 3,771
  • 2
  • 24
  • 36
  • 4
    This doesn't seem to be the case: in WebKit at least, the `load` event can fire before web fonts have loaded. – Tim Down Jun 11 '13 at 10:39