Paul Irish

Making the www great

@font-face Feature Detection

As I’m piecing together a very comprehensive solution for using custom typefaces online, one of the crucial aspects is determining what browsers support @font-face.

My requirements for this detection were:

  • No browser userAgent sniffing
  • No extra HTTP request required
  • The test must be synchronous, no race conditions or HTTP requests
  • Must be performant with a small footprint, natch
  • Results should match the latest research on compatibility

I quickly tried:

1
!!window.CSSFontFaceRule

This test works great in FF2+, Safari and Opera. But it fails in IE (bug surprise) and Chrome gives a false positive.

The code

What follows is the best test for @font-face support I have found:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
/*!
 * isFontFaceSupported - v0.9 - 12/19/2009
 * http://paulirish.com/2009/font-face-feature-detection/
 *
 * Copyright (c) 2009 Paul Irish
 * MIT license
 */

var isFontFaceSupported = (function(){


    var fontret,
        fontfaceCheckDelay = 100;

      // IE supports EOT and has had EOT support since IE 5.
      // This is a proprietary standard (ATOW) and thus this off-spec,
      // proprietary test for it is acceptable.
    if (!(![email protected][email protected](@_jscript_version>=5)[email protected]@*/0)) fontret = true;

    else {

    // Create variables for dedicated @font-face test
      var doc = document, docElement = doc.documentElement,
          st  = doc.createElement('style'),
          spn = doc.createElement('span'),
          wid, nwid, body = doc.body,
          callback, isCallbackCalled;

      // The following is a font, only containing the - character. Thanks Ethan Dunham.
      st.textContent = "@font-face{font-family:testfont;src:url(data:font/opentype;base64,T1RUTwALAIAAAwAwQ0ZGIMA92IQAAAVAAAAAyUZGVE1VeVesAAAGLAAAABxHREVGADAABAAABgwAAAAgT1MvMlBHT5sAAAEgAAAAYGNtYXAATQPNAAAD1AAAAUpoZWFk8QMKmwAAALwAAAA2aGhlYQS/BDgAAAD0AAAAJGhtdHgHKQAAAAAGSAAAAAxtYXhwAANQAAAAARgAAAAGbmFtZR8kCUMAAAGAAAACUnBvc3T/uAAyAAAFIAAAACAAAQAAAAEAQVTDUm9fDzz1AAsD6AAAAADHUuOGAAAAAMdS44YAAADzAz8BdgAAAAgAAgAAAAAAAAABAAABdgDzAAkDQQAAAAADPwABAAAAAAAAAAAAAAAAAAAAAwAAUAAAAwAAAAICmgGQAAUAAAK8AooAAACMArwCigAAAd0AMgD6AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAEZIRAAAQAAgAC0C7v8GAAABdv8NAAAAAQAAAAAAAAAAACAAIAABAAAAFAD2AAEAAAAAAAAAPAB6AAEAAAAAAAEAAgC9AAEAAAAAAAIABwDQAAEAAAAAAAMAEQD8AAEAAAAAAAQAAwEWAAEAAAAAAAUABQEmAAEAAAAAAAYAAgEyAAEAAAAAAA0AAQE5AAEAAAAAABAAAgFBAAEAAAAAABEABwFUAAMAAQQJAAAAeAAAAAMAAQQJAAEABAC3AAMAAQQJAAIADgDAAAMAAQQJAAMAIgDYAAMAAQQJAAQABgEOAAMAAQQJAAUACgEaAAMAAQQJAAYABAEsAAMAAQQJAA0AAgE1AAMAAQQJABAABAE7AAMAAQQJABEADgFEAEcAZQBuAGUAcgBhAHQAZQBkACAAaQBuACAAMgAwADAAOQAgAGIAeQAgAEYAbwBuAHQATABhAGIAIABTAHQAdQBkAGkAbwAuACAAQwBvAHAAeQByAGkAZwBoAHQAIABpAG4AZgBvACAAcABlAG4AZABpAG4AZwAuAABHZW5lcmF0ZWQgaW4gMjAwOSBieSBGb250TGFiIFN0dWRpby4gQ29weXJpZ2h0IGluZm8gcGVuZGluZy4AAFAASQAAUEkAAFIAZQBnAHUAbABhAHIAAFJlZ3VsYXIAAEYATwBOAFQATABBAEIAOgBPAFQARgBFAFgAUABPAFIAVAAARk9OVExBQjpPVEZFWFBPUlQAAFAASQAgAABQSSAAADEALgAwADAAMAAAMS4wMDAAAFAASQAAUEkAACAAACAAAFAASQAAUEkAAFIAZQBnAHUAbABhAHIAAFJlZ3VsYXIAAAAAAAADAAAAAwAAABwAAQAAAAAARAADAAEAAAAcAAQAKAAAAAYABAABAAIAIAAt//8AAAAgAC3////h/9UAAQAAAAAAAAAAAQYAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAAAA/7UAMgAAAAAAAAAAAAAAAAAAAAAAAAAAAQAEBAABAQEDUEkAAQIAAQAu+BAA+BsB+BwC+B0D+BgEWQwDi/eH+dP4CgUcAIwPHAAAEBwAkREcAB4cAKsSAAMCAAEAPQA/AEFHZW5lcmF0ZWQgaW4gMjAwOSBieSBGb250TGFiIFN0dWRpby4gQ29weXJpZ2h0IGluZm8gcGVuZGluZy5QSVBJAAAAAAEADgADAQECAxQODvb3h/cXAfeHBPnT9xf90wYO+IgU+WoVHgoDliX/DAmLDAr3Fwr3FwwMHgoG/wwSAAAAAAEAAAAOAAAAGAAAAAAAAgABAAEAAgABAAQAAAACAAAAAAABAAAAAMbULpkAAAAAx1KUiQAAAADHUpSJAfQAAAH0AAADQQAA)}";
      doc.getElementsByTagName('head')[0].appendChild(st);


      spn.setAttribute('style','font:99px _,serif;position:absolute;visibility:hidden');

      if  (!body){
        body = docElement.appendChild(doc.createElement('fontface'));
      }

      // the data-uri'd font only has the - character
      spn.innerHTML = '-------';
      spn.id        = 'fonttest';

      body.appendChild(spn);
      wid = spn.offsetWidth;

      spn.style.font = '99px testfont,_,serif';

      // needed for the CSSFontFaceRule false positives (ff3, chrome, op9)
      fontret = wid !== spn.offsetWidth;

      var delayedCheck = function(){
        if (isCallbackCalled) return;
        fontret = wid !== spn.offsetWidth;
        callback && (isCallbackCalled = true) && callback(fontret);
      }

      addEventListener('load',delayedCheck,false);
      setTimeout(delayedCheck,fontfaceCheckDelay);
    }

    function ret(){  return fontret || wid !== spn.offsetWidth; };

    // allow for a callback
    ret.ready = function(fn){
      (isCallbackCalled || fontret) ? fn(fontret) : (callback = fn);
    }

    return ret;
})();

Download from github

The latest is always at: http://github.com/paulirish/font-face-detect
isFontFaceSupported.js Uncompressed - 4.3k
isFontFaceSupported.min.js Compressed - 3.1k

Usage

1
2
3
4
5
6
7
isFontFaceSupported() // will return a boolean indicating support

// you can also use with a callback,
//  it will be called 100ms later which is adaquate for Gecko and Webkit to properly use the data-uri'd font.
isFontFaceSupported.ready(function(bool){
  // bool is a boolean that indicates support
});

Sorry. :(

You’ll spot the IE conditional compilation in there. I don’t like it either, but I’m unaware of any other workable approach (that doesn’t pull in an .eot) to test for @font-face support. If you have an idea, please share it!

On the approach

I first use the Web Font Optimizer to subset a truetype font to only contain the period (.) character (2.2k file!), then send it through a data URI converter, then chuck it into a style tag. I test the width of a span of text without the custom font, and then again with the custom font. If the values are different, we can assume @font-face is supported and works.

With a trip through the YUI Compressor, the script is 3.5k3.1k. If you have any ideas on bringing that figure down, I’d love to hear ‘em.

Great! Any disadvantages?

Yeah there’s one big one. Both Gecko and Webkit load in a data-uri font asynchronously, so the test may give a false negative if you call isFontFaceSupported() immediately afterwards:

1
2
3
4
<script src="isFontFaceSupported.min.js"></script>
<script>
  if (isFontFaceSupported()) ... // this may report a false negative.
 // that's why we have the isFontFaceSupported.ready() callback mechanism

I’m not terribly happy with this asynchronous delay, so I’ve written an alternative that uses browser userAgent sniffing. This practice is not recommended and is not terribly future-proof, but it’s the only synchronous solution available.

isFontFaceSupported() - sniffing variant
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/*!
 * isFontFaceSupported - Sniff variant - v0.9 - 12/19/2009
 * http://paulirish.com/2009/font-face-feature-detection/
 *
 * Copyright (c) 2009 Paul Irish
 * MIT license
 */

/* Browser sniffing is bad. You should use feature detection.
   Sadly the only feature detect for @font-face is
   asynchronous. So for those that *need* a synchronous solution,
   here is a sniff-based result:
*/

var isFontFaceSupported = function(){

  var ua = navigator.userAgent, parsed;

  if ([email protected][email protected](@_jscript_version>=5)[email protected]@*/0)
      return true;
  if (parsed = ua.match(/Chrome\/(\d+\.\d+\.\d+\.\d+)/))
      return parsed[1] >= '4.0.249.4';
  if ((parsed = ua.match(/Safari\/(\d+\.\d+)/)) && !/iPhone/.test(ua))
      return parsed[1] >= '525.13';
  if (/Opera/.test({}.toString.call(window.opera)))
      return opera.version() >= '10.00';
  if (parsed = ua.match(/rv:(\d+\.\d+\.\d+)[^b].*Gecko\//))
      return parsed[1] >= '1.9.1';

  return false;

}

This guy is also on github

2009.09.24: updated the code, and threw it all on github. callback style makes its debut here. This code matches the exact same implementation that’s in Modernizr 1.0

2009.12.18: Added a useragent sniffing alternative for those who want reliable synchronous detection.

2009.12.19: New, smaller font file (Thanks Ethan Dunham). The file is now 15% smaller. Script does not remove the extra element it adds to the DOM now, as to assure more accurate results.
2010.11.02: I now recommend a different technique.. It’s from Diego Perini.. The code is below:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
var isFontFaceSupported = (function(){
var
sheet, doc = document,
head = doc.head || doc.getElementsByTagName('head')[0] || docElement,
style = doc.createElement("style"),
impl = doc.implementation || { hasFeature: function() { return false; } };

style.type = 'text/css';
head.insertBefore(style, head.firstChild);
sheet = style.sheet || style.styleSheet;

var supportAtRule = impl.hasFeature('CSS2', '') ?
        function(rule) {
            if (!(sheet && rule)) return false;
            var result = false;
            try {
                sheet.insertRule(rule, 0);
                result = !(/unknown/i).test(sheet.cssRules[0].cssText);
                sheet.deleteRule(sheet.cssRules.length - 1);
            } catch(e) { }
            return result;
        } :
        function(rule) {
            if (!(sheet && rule)) return false;
            sheet.cssText = rule;

            return sheet.cssText.length !== 0 && !(/unknown/i).test(sheet.cssText) &&
              sheet.cssText
                    .replace(/\r+|\n+/g, '')
                    .indexOf(rule.split(' ')[0]) === 0;
        };

return supportAtRule('@font-face { font-family: "font"; src: "font.ttf"; }');
})();

Comments