Detecting Browser CSS Style Support
May 3, 2009
Now I’d normally be the first one to speak out in favour of unobtrusive JavaScript and attempting to keep CSS styling out of JavaScript. Rarely should you need to directly impose CSS styles on an element via the style object, after all that is what class names are for. However there are some styles that are commonly and justifiably manipulated within JavaScript; display, visibility, width, height, top, and left are just a few of the styles often directly manipulated within JavaScript typically for some type of UI component. Of course, some of these styles are not supported in all the mainstream browsers, such as opacity in IE and -moz-transform in Firefox < 3.5.
Since browser detection is entirely unreliable and unnecessary, we need to find something better. Therefore, we will instead test to see if the browser is capable of computing the style we are trying to determine support for.
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 (stylesheets, inline styles, etc.) 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 computed/parsed. As with runtimeStyle in IE, only unsupported styles will return a value of undefined when we check the element’s computed style:
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 against multiple tests of the same style 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 assignable style values? Fixed positioning, currently not supported in IE6, can be a valuable style for UI components such as overlays, so how can we test for that? Rather than using browser detection or adding an event listener to dynamically reposition the overlay every time the user scrolls the window, we can just employ fixed positioning for those browsers that support it!
Amazingly, IE 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.
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 have yet to encounter a single false positive in any case. So to help prove the reliability, 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 free-form text inputs so you can try any styles you wish.
Hopefully we’ll see some of the developers of the mainstream libraries take notice and employ these 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 such as fixed positioning.