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 (!(!/*@cc_on@if(@_jscript_version>=5)!@end@*/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,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)}";
      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 (/*@cc_on@if(@_jscript_version>=5)!@end@*/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"; }');
})();

.net Magazine - May Issue

Back in the May issue of .net magazine, my friend and talented designer Tom Kershaw wrote an article on improving your speed, efficiency and polish with Photoshop. [PDF download here]

I wrote a sidebar about better ways for front-end developers to work with visual designers. Funnily enough, because of the lead time, by the time the magazine got to press, some of my recommendations (e.g. “embrace sIFR”) were already out of date.

If anyone has additional recommendations on how front-end developers and designers can improve their workflow, I’d love to see them in the comments.

Caching and Google’s Ajax Libraries API – Cache Your jQuery

Leveraging Google’s free hosting of common javascript libraries seems to be getting more popular. While you should be serving all javascript concatenated and minified together, the free hosting is nice for quick jobs or more minimal projects.

The versioning system they devised is a very clever way to always serve you the most up-to-date script. For example, you request version 2 of SWFObject and it’ll currently deliver 2.2, but in the future you’ll always get the most recent 2.x release.

I did a bit of research on how they handle these cases, and since these facts aren’t elsewhere, they needed a home. :)

The current caching rules are as follows:

RequestResponse cached for
/1.3.2/jquery.min.jsone year
/1.3/jquery.min.jsno caching
/1/jquery.min.jsone hour

jQuery URLs used only for illustration purposes. It’s the same case with all scripts’ minor/major versions Minified and unminified files are treated the same

Previous to now, using the google.load('jquery','1.2.6') technique was the only way to ensure the script stayed considerably cached. Direct path-based access scripts were only cached for one day. But no worries, because all is better now.

2010.08.25 - Updated caching times. How odd..