Detecting Browser CSS Style Support
If you have ever wanted to manipulate those brand spanking new styles such as -moz-transform, -webkit-transition, or even some older ones like opacity in IE and max-width in IE6 but didn’t because you were reluctant of browser support, than you are not alone. For this article, I will be exploring some unique tactics in an effort to solve the dilemma as it relates to JavaScript.
Since browser detection is entirely unreliable and unnecessary, we need to find something better. Instead we will employ feature testing to ascertain the browser’s capability of computing the style we are trying to determine support for. The method here is simple, if the browser can interpret the style than it must be supported, otherwise it must not.
In IE we can use the little known runtimeStyle object inherit to all elements in IE, it allows us to set the cascaded style (represented in currentStyle) which will override any and all other styling influences on the element. However we won’t be using runtimeStyle to set the style, but to check the value it holds to determine whether or not the style was properly computed. By simply examining the value held for the style in the runtimeStyle object, we find that it returns undefined for unsupported styles:
typeof el.runtimeStyle.width !== 'undefined'; // true typeof el.runtimeStyle.opacity !== 'undefined'; // false
As for any standards compliant browsers, we check for support in a similar fashion; using the getComputedStyle method we can check to see if the style is capable of being interpreted. As with runtimeStyle in IE, only unsupported styles will return a value of undefined:
typeof view.getComputedStyle(el, null).test !== 'undefined'; // false typeof view.getComputedStyle(el, null).minWidth !== 'undefined'; // true
Thats it! When we combine these techniques we get a robust method to detect CSS style support in all the mainstream browsers. Throw in some caching for increased performance and isStyleSupported is born:
isStyleSupported = (function(){ var cache = {}; var el = document.createElement('div'); return function(style){ if(style in cache){ return cache[style]; }else{ var supported = false; if(el.runtimeStyle){ supported = typeof el.runtimeStyle[style] !== 'undefined'; }else{ var view = document.defaultView; if(view && view.getComputedStyle){ var cs = view.getComputedStyle(el, null)[style]; supported = typeof cs !== 'undefined'; } } return cache[style] = supported; } } })();
Perfect right? Well not so fast, we don’t have to stop there, why not extend the method to not only determine support for styles, but their assignable values? Multiple backgrounds, gradients, and the rgba specifications are just a few of the values new to CSS3 that we can leverage given a reliable test case.
Amazingly, IE again makes this quite easy using the technique we’ve already employed for determining style support. All we need to add is value assignment via the style object and wrap the test case in a try-catch block as IE will throw an error for unsupported style values.
try{ el.style.width = '12px'; supported = typeof el.runtimeStyle.width !== 'undefined'; // true el.style.width = 'abcd'; supported = typeof el.runtimeStyle.width !== 'undefined'; // false }catch(e){};
However, if we are looking to determine support for the style values in standards compliant browsers, than getComputedStyle proves to be unreliable. Take for instance any number of scalable styles such as width and height, the method will always convert any units back to pixels, meaning a quick comparison would fail:
el.style.width = "1em"; document.defaultView.getComputedStyle(el, null).width; // 16px
In addition to that testing against unsupported style values will not always return a definitive result where as support can be easily determined. For example, setting an element’s color to an unrecognized value will always result in the return of the rgb representation of the color black:
el.style.color = "test"; document.defaultView.getComputedStyle(el, null).color; // rgb(0,0,0)
To quickly resolve this and avoid making some elaborate parsing method, we need to use a secondary test case. We create a new element as part of our base element’s innerHTML and add the styling we wish to test for as inline styles. When we retrieve the new element and examine its style, we find that unsupported style values will always return an empty string:
el.innerHTML = '<div style="color:test ; width:10px"></div>'; el.firstChild.style.color !== ""; // false el.firstChild.style.width !== ""; // true
Using this technique means any styles supplied must be in standard hyphenated CSS format (background-color) as opposed to the previous method which must be handed camel-cased styles (backgroundColor). This means that we need to include a function to handle camel case conversions; a small hit on performance yes, but also some syntastic sugar for the method – give and take I suppose.
Throwing these new techniques together results in a method capable of determining support for not only styles but their supported assignable/recognizable values by supplying an optional second parameter.
isStyleSupported = (function(){ var cache = {}; var el = document.createElement('div'); var toCamelCase = function(str){ var parts = str.split('-'), camel = parts[0]; for(var i=1, len=parts.length; i < len; i++){ camel += parts[i].charAt(0).toUpperCase() + parts[i].substring(1); } return camel; }; return function(style, value){ var key = style + value || ""; if(key in cache){ return cache[key]; }else{ var supported = false; var camel = toCamelCase(style); if(el.runtimeStyle){ try{ el.style[camel] = value || ""; supported = typeof el.runtimeStyle[camel] !== 'undefined'; }catch(e){}; }else{ var view = document.defaultView; if(view && view.getComputedStyle){ var cs = view.getComputedStyle(el, null)[camel]; supported = typeof cs !== 'undefined'; if(value){ el.innerHTML = '<div style="'+style+':'+value+'"></div>'; supported = supported && el.firstChild.style[camel] !== ""; } } } return cache[key] = supported; } } })();
So how reliable are the methods? Well in all my testing in all the browsers I could get my hands on – I encountered just one false positive, that being fixed positioning in IE6, something I am still struggling to explain. To test the reliability yourself, I have put together a simple test page which includes various styles and their corresponding results when tested against isStyleSupported. In addition to that, I have included two text inputs so you can try and test any styles you wish.
Hopefully we’ll see some of the developers of the mainstream libraries take notice and employ these or similar techniques in future versions of their respective frameworks and finally rid themselves of the unreliable browser detection so many of them still utilize today in cases such as this.
I like the concept and actually there’s a similar discussion at kangax’s blog.
There are some minor things that I want to point out. Both philosophically and coding-wise.
Philosophically: the camelCase function is evil, it’s more evil when you use getStyle and setStyle inside an animation loop. I would prefer to educate coders in the documentation to camelCase their CSS properties. I realized that when looking at your animation library.
Coding-wise: typeof xxx === “undefined”;
1. Typeof always return strings, strict === is not needed, and the performance penalty is minimal, and I don’t follow my own advice.
2. Munging undefine is faster than using typeof
var undefined; // no values associated
xxx === undefined; // returns true if xxx is undefined
Looking at some of your previous posts, I am wondering why you did not create a complete DOM library since I think you have enough knowledge to do so.
Oh BTW, I gained some insights into DOM animation through the understanding of how your animation library works. I made a library out of that it’s hosted at github.
http://github.com/kltan/short/tree/2d4fb83712aaf76ecaf9dfd1c0ad43fcef9e168f/source
Have a nice day.
@Kean Tan
The camel case method was added solely for syntactic sugar but your absolutely right, that is all it is really good for. I feel the real mistake (in the animation library) is the requirement to use hyphenated styles such as background-color. I’ll be sure to fix that in the next version due out later this month.
As for typeof undefined, I was aware of both points you brought up but I suppose I got a little caught up in my strict type checking to put two and two together.
What you recommended reminds me of the window["undefined"] = window["undefined"] hack (although I’m not sure if its appropriate to call it a hack). Actually, I’ve recently started to use the “in” operator to perform checks against undefined values, (“prop” in cache).
To the point of writing my own library; I have considered it but ultimately felt there was nothing I could offer somebody else hasn’t already.
Not to mention I am under the belief that every project deserves its own library; it would be a bit hypocritical of me to release my own. This belief and my eagerness to get involved in the open source community brought me to one of the few voids in the community; standalone and lightweight modules. I already plan on releasing a whole slew of modules in the future.
Great job on the library too! Glad my animation library served you well.
Thanks!
It’s seems that in code is some bug. The test page told me that IE7 do not support opacity. That’s not true.
@Cezary Tomczyk
The test page is correct, IE7 only supports opacity through the use of filters. The opacity property is not supported in any version of IE. See here: http://www.quirksmode.org/css/opacity.html
You where right. I forgot about it. I become accustomed to normal browsers.
// Another camelcase function :
String.prototype.camelcase = function()
{
return this
.toString()
.toLowerCase()
.replace(/(-|\.|\s)(\w)/g, function(a,b,c)
{
return c.toUpperCase();
}) ;
}
console.log( ‘ www-xxx.yyy zzz’)
@Xavier
The only problem with your implementation is that the replace method of strings do not support a function as a second parameter in Safari 2.0.2.
Hi Ryan,
Have you seen Modernizr? It’s a library that does, well, precisely this kinda stuff
http://www.modernizr.com/
(I know, this seems spammy, but it seems to me like you could save some time and effort here…
)
@Faruk Ateş
I have seen Modernizr, the important difference between it and my solution is that my solution has the ability to determine support for any styles you provide the method as opposed to Modernizr’s select few.
Also, my solution is capable of determining support for assignable style values.
This is actually what I need. Thanks
I agree modernizr is a different solution. With modernizr you cannot tell easily what browser is being used. It just tells you what is supported and what isn’t but in a generic way. For example something like .box-model in modernizr means that box-model is supported, but you dont know if that means you are in Webkit or FireFox, which both implement the box model a bit differently(this is actually my problem).
With this library isStyleSupported you can check specifically for -moz-box-orient or -webkit-box-orient this will let you know EXACTLY what browser you are and which implementation of box model you are working with.
Of course if it is an older browsers this method won’t work but im not supporting old browsers so that is ok with me.