Perfection kills

Exploring Javascript by example

Feature testing CSS properties

May 11th, 2009 by kangax (Permalink)

Contrary to many beliefs, detecting CSS properties support with Javascript is not very complicated. One of the CSS-related tests in Common Feature Tests suite is IS_CSS_BORDER_RADIUS_SUPPORTED which looks as simple as this:

var IS_CSS_BORDER_RADIUS_SUPPORTED = (function() {
  var docEl = document.documentElement, s;
  if (docEl && (s = docEl.style)) {
      return typeof s.borderRadius == "string" 
        || typeof s.MozBorderRadius == "string" 
        || typeof s.WebkitBorderRadius == "string" 
        || typeof s.KhtmlBorderRadius == "string";
  }
  return null;
})();

As you can see, testing a CSS property boils down to an inference drawn from the presence of the same named property in a style of an arbitrary element. If you wanted to test support for a “marginLeft” property, you would do it like so:

var el = document.createElement('div');
typeof el.style.marginLeft == 'string'; // true
typeof el.style.marginLeft2 == 'string'; // false

An alternative (and less verbose) way involves replacing typeof operator with in operator:

var el = document.createElement('div');
'marginLeft' in el.style; // true
'marginLeft2' in el.style; // false

in was actually used in earlier versions of CFT but I find such inference too weak. in doesn’t check type of a property; it merely determines property existence, and would return true even if it had undefined (or any other, non-string) value.

IS_CSS_BORDER_RADIUS_SUPPORTED doesn’t just test CSS3 “borderRadius” property. It also tries some of the known proprietary, vendor-specific variations – “MozBorderRadius”, “WebkitBorderRadius” and “KhtmlBorderRadius”. While tweaking this test a couple of days ago, I realized that if I wanted to test CSS3 “boxShadow” property, I would need to follow the same logic and “iterate” over the very same prefixes – “MozBoxShadow”, “WebkitBoxShadow” and so on. Such duplication was clearly not the way to go and a more generic function seemed like a better solution.

getStyleProperty

What it all led to was a very simple getStyleProperty helper:

var getStyleProperty = (function(){
 
  var prefixes = ['Moz', 'Webkit', 'Khtml', 'O', 'Ms'];
 
  function getStyleProperty(propName, element) {
    element = element || document.documentElement;
    var style = element.style,
        prefixed;
 
    // test standard property first
    if (typeof style[propName] == 'string') return propName;
 
    // capitalize
    propName = propName.charAt(0).toUpperCase() + propName.slice(1);
 
    // test vendor specific properties
    for (var i=0, l=prefixes.length; i<l; i++) {
      prefixed = prefixes[i] + propName;
      if (typeof style[prefixed] == 'string') return prefixed;
    }
  }
 
  return getStyleProperty;
})();

getStyleProperty follows the same logic and returns a first found CSS property on an arbitrary (or optionally specified) element. If you run getStyleProperty('borderRadius') in Mozilla-based browser, it would return “MozBorderRadius”; if it was a Webkit-based client, a “WebkitBorderRadius” would be returned. Finally, if no property was found, methods would exit with undefined.

Simple as that.

The way you would test a property is by comparing returned result’s type to “string”:

if (typeof getStyleProperty('borderRadius') == 'string') {
  // property is supported
}

I didn’t want to recreate a prefixes array every time function is called and stored it in a closure. I also used function declaration inside that closure, rather than returning an anonymous function expression, so that getStyleProperty had a descriptive identifier. There’s a document.documentElement used as a generic element (in case none is provided as a second argument), but I could as well have created a new one with document.createElement.

You’re obviously free to improvise with these subtleties as you find appropriate, as long as the actual testing mechanism is left intact.

Performance considerations

I don’t consider execution speed very crucial for a method such asgetStyleProperty, since it would most likely be executed once or twice and probably during the load time. Nevertheless, two basic optimizations come to mind.

Caching a property is one of them. We can simply create a “private” object that maps original (passed into function) property to an actual (possibly prefixed) one and use that object as a cache:

var getStyleProperty = (function(){
 
  var prefixes = ['Moz', 'Webkit', 'Khtml', 'O', 'Ms'];
  var _cache = { };
 
  function getStyleProperty(propName, element) {
    element = element || document.documentElement;
    var style = element.style,
        prefixed,
        uPropName;
 
    // check cache only when no element is given
    if (arguments.length == 1 && typeof _cache[propName] == 'string') {
      return _cache[propName];
    }
    // test standard property first
    if (typeof style[propName] == 'string') {
      return (_cache[propName] = propName);
    }
 
    // capitalize
    uPropName = propName.charAt(0).toUpperCase() + propName.slice(1);
 
    // test vendor specific properties
    for (var i=0, l=prefixes.length; i<l; i++) {
      prefixed = prefixes[i] + uPropName;
      if (typeof style[prefixed] == 'string') {
        return (_cache[propName] = prefixed);
      }
    }
  }
 
  return getStyleProperty;
})();

Another optimization is to cache vendor prefix, such as “Moz” or “Webkit” rather than a specific property. An assumption we’re making here is that if one prefixed property is found in an element’s style, than that prefix can safely be prepended to any other property resulting in a “proper” string. I’m not sure if such inference is a good thing, since clients are obviously not limited to implementing only one type of vendor-specific properties; I can imagine Khtml-based clients implementing both – Khtml- and Webkit- properties.

It might be safer to just stick to “regular” caching mechanism (if any at all), as in the previous snippet.

Inference downsides

Theoretically speaking, the inference we are relying on for getStyleProperty is not all that strong. A mere existence of a CSS property doesn’t tell us about an actual implementation and its conformance to a specification. A browser might have “borderRadius” property with a proper string value; it could allow to assign to that property and even set its value to a specified one after assignment; yet, it could never make borders rounded. The problem is that many CSS3 declarations affect document in such way that it is impossible to detect their effect on a DOM. Border’s radius and box’s shadow, text’s stroke and text overflow (ending with an ellipsis) are all purely visual aspects. If “marginLeft” conformance can be checked by testing element’s offset (as it is represented in a DOM), most of the CSS3 properties don’t provide such luxury.

This is an important thing to remember.

CFT

getStyleProperty is now part of CFT suite (source of which you can find on github). There’s also a simple test page with some of the common CSS3 properties tested.

As always, I’d love to hear any suggestions/corrections you have.

Categories: Uncategorized, cft, sniff busting 11 Comments »

Comments (11)

  1. Gravatar

    unscriptable said:

    Good stuff and nice implementation. This gets us one step closer to the elimination of browser sniffing!

    I hope we’d never have to worry about mixing khtml- and webkit- prefixes. That’d be insane!

    Nice use of closure-based memoization, too. :-)

  2. Gravatar

    Andrea Giammarchi said:

    nice one, I used the typeof string in vice-versa as well so at least there are two of us in troubles if this trick will fail with some weird browser :D

    About “cache”, I think less operations there are before possible match, better the cache will perform. The same is for element, I would have use a different approach for a more clever cache object, something like this:

    var getStyleProperty = (function(){
    var prefixes = ['Moz', 'Webkit', 'Khtml', 'O', 'Ms'],
    _cache = { };
    function getStyleProperty(propName, style) {
    if(typeof style[propName] == 'string')
    return propName;
    for(var
    uPropName = propName.charAt(0).toUpperCase() + propName.slice(1),
    i=0, l=prefixes.length,
    prefixed;
    i
    based on typeof, if the cache is true or simply a number different from zero it should work without problems. what do you think?

  3. Gravatar

    Andrea Giammarchi said:

    rewind:
    nice one, I used the typeof string in vice-versa as well so at least there are two of us in troubles if this trick will fail with some weird browser :D

    About “cache”, I think less operations there are before possible matches, better the cache will perform. The same is for element, I would have used a different approach for a more clever cache object, something like this:

    
    var getStyleProperty = (function(){
      var prefixes = ['Moz', 'Webkit', 'Khtml', 'O', 'Ms'],
          _cache = { };
      function getStyleProperty(propName, style) {
        if(typeof style[propName] == 'string')
          return propName;
        for(var
          uPropName = propName.charAt(0).toUpperCase() + propName.slice(1),
          i=0, l=prefixes.length,
          prefixed;
          i < l; ++i
        ) {
          if(typeof style[prefixed = prefixes[i] + uPropName] == 'string')
            return prefixed;
        };
        return -1;
      };
      return function(propName, element){
        var key = propName + (element = element || document.documentElement).nodeName;
        return _cache[key] || (_cache[key] = getStyleProperty(propName, element.style));
      };
    })();
    
    

    based on typeof, if the cache is true or simply a number different from zero it should work without problems. what do you think?

  4. Gravatar

    Aaron Gustafson said:

    It’s interesting to see how you’ve gone about this, Juriy, as I’ve taken a slightly different tack with eCSStender’s isSupported() method.

  5. Gravatar

    Ryan Morr said:

    Nice work!! I created a similar function over at my blog and is able to determine support for not only CSS styles but their supported assignable values, such as fixed positioning. I rather like your approach; much simpler compared to some of the techniques I’ve employed, although I was able to avoid that bit of vender-specific ugliness. Find it here: http://ryanmorr.com/archives/detecting-browser-css-style-support

  6. Gravatar

    kangax (article author) said:

    Ryan, that’s very interesting. I’ll take a look at your implementation whenever I get a chance.

  7. Gravatar

    Kean said:

    On IE, the unload event needs window to test on.

  8. Gravatar

    kangax (article author) said:

    @Kean

    Yep, that’s exactly what I’m doing in isEventSupported demo (see line 30)

  9. Gravatar

    Kean said:

    Comment on wrong post, I am referring to “Detecting event support without browser sniffing”

  10. Gravatar

    kangax (article author) said:

    @Kean
    I figured : )

  11. Gravatar

    eyelidlessness said:

    I’m not sure if such inference is a good thing, since clients are obviously not limited to implementing only one type of vendor-specific properties; I can imagine Khtml-based clients implementing both – Khtml- and Webkit- properties.

    Safari does still support -khtml- prefixes (but it seems to also translate new -webkit- prefixed styles when specified with -khtml-, but who knows if this is supported across the board), so that’s a sound warning.

Trackbacks

Leave a Comment

Allowed tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre lang="" line="" escaped="">

Please, don't forget to escape your input (<, > and &). Wrap code sections with <pre>