Sniff my browser: The Modernizr inadequacy

Published 20:03 on 17 May, 2011

I’m currently involved in a project to write a fairly extensive set of best practices for front-end development. Alongside myself, this project includes input from a fair cross-section of my peers in the front-end development community. These best practices will be implemented alongside a coding standard as standards for development within the organisation I work for, and hopefully many other organisations when they are published.

Of all the standards that a front-end team might want to implement, those that concern the identification and graceful degradation of cross-browser feature sets can be the hardest to define.

With that in mind, I’ve been poking around the front-end community looking for possible solutions. By far the most common approach—and one that gains an astounding level of attention in the community—is to implement Modernizr, a JavaScript feature sniffer created by Faruk Ateş, Paul Irish and Alex Sexton.

Unfortunately, despite my respect for the developers involved, I just can’t advocate Modernizr as a solution. Let me explain why; but first, let’s revisit some concepts that are going to be quite relevant…

The web standards trifle

The separation of structure, presentation, and dynamic behaviour is imperative to web standards. To that end, Andy “Malarkey” Clarke wrote a fantastic post detailing a method he liked to use to explain this fact to clients. He entitled it “the web standards trifle”.

In Andy’s metaphor, the separation of concerns can be imagined as follows:

  1. Sponge

    This layer is your content, marked-up with well structured and valid semantic HTML. This HTML provides more information about each piece of content and allows any device reading the code to output as appropriate.

  2. Fruity jelly

    This is your presentational layer. This separates all your presentational code into CSS, potentially allowing a user to override it with their own, or for you to serve something different based on context, e.g. a print or mobile stylesheet.

  3. Custard

    This is the layer that accommodates any on-page behaviours. In the world of web standards we use this layer to progressively enhance our HTML and CSS layers with more interactive magic. I’ll come back to this later.

The true virtue of this divide is that the presentational (jelly) and behavioural (custard) layers can potentially be removed or overridden without affecting each other, or the content and markup beneath. This is hugely important if we want our websites to maintain client agnosticism and improve accessibility.

Progressive enhancement

Following the publication of this article there have been several suggestions that I explain the difference between graceful degradation and progressive enhancement. Suffice to say, this is something I have covered previously in another article.

Browser vs. Feature Sniffing

Browser “sniffing” is the act of attempting to discern a user’s browser by some means. This “awareness” can then be used in conditional code to control output or interaction for a specific browser. This technique was used a great deal in days of yore, when browsers had very different rendering engines and didn’t all conform to web standards. However, it was only ever really required in JavaScript.

The old school way:

if (document.all) {
    // IE4
    height = document.body.offsetHeight;
} else if (document.layers) {
    // NN4
    height = window.innerHeight;
} else {
    // other
    height = 0;
}

Not a good solution

There are problems with the method used above; for one thing, it is not the most direct method of identifying a browser. However, putting that aside for the moment, there are issues even with the theory of browser sniffing:

It’s quite obvious that browser sniffing does not scale. As new browsers are released, the sniffing code will need updating.

To make matters worse, many manufacturers developed their browsers to identify themselves incorrectly to get around legacy browser sniffing code. This ultimately meant that sometimes browsers were misidentified.

Ultimately browser sniffing is not a good solution. If you adopt it, you will usually end up maintaining separate streams of development whenever you need to add or update features. There are ways to mitigate that pain, but what if there was a better solution?

Feature sniffing

A more scalable approach is to use object detection in JavaScript to discover if a feature is available before you use it. An example of this might be:

if (document.querySelector) {
    element = document.querySelector(selectors);
}

The obvious advantage here is that it is browser agnostic; there are no assumptions as to which browser is involved at all. Using this technique, we overcome the scalability and maintenance headaches incurred through browser sniffing, and we can develop for graceful degradation from the outset.

CSS is a DSL that lacks features

Now, that’s all very well and good when we’re talking about JavaScript, but what happens when we need to affect the presentation layer?

CSS is a very simple domain specific language (DSL) that functions as a series of rules that override each other in a variety of ways (the cascade). There is no mechanism to detect a specific feature or a user-agent; only media types and potentially the odd CSS hack where a user-agent has imperfectly implemented an interpreter. There are also no conditionals; you just have to use the cascade to provide alternatives based on specificity or even just source order.

With these limitations, it is impossible to implement either browser or feature sniffing in CSS. This means we need to find an alternative method to affect the cascade based on the user’s feature set.

The Modernizr method

Step in Modernizr. In the words of the documentation:

Modernizr aims to bring an end to the UA sniffing practice. Using feature detection is a much more reliable mechanic to establish what you can and cannot do in the current browser, and Modernizr makes it convenient for you in a variety of ways:

  1. It tests for over 20 next-generation features, all in a matter of milliseconds.

  2. It creates a JavaScript object (named Modernizr) that contains the results of these tests as boolean properties.

  3. It adds classes to the html element that explain precisely what features are and are not natively supported.

Following along so far? For the most part, that all seems fairly good. What’s more, Modernizr is definitely being pushed as the solution of choice amongst the front-end community:

So with all that in mind, let’s go back to why I will not be recommending Modernizr as a best practice:

Sniffing CSS features with JavaScript

The real issue is that Modernizr uses JavaScript to do the feature sniffing. This method is absolutely fine if we were only looking to test for the features in JavaScript. However, Modernizr is marketed on its ability to detect CSS3 and HTML5 features so that you can write gracefully degrading CSS:

Modernizr is a small and simple JavaScript library that helps you take advantage of emerging web technologies (CSS3, HTML 5) while still maintaining a fine level of control over older browsers that may not yet support these new technologies.

If Modernizr had sold itself as a JavaScript feature sniffing solution that also detected CSS features perhaps I could be more forgiving. In that context, it would be the developers’ responsibility to use Modernizr correctly. However, since the Modernizr site and documentation are selling it specifically as a CSS feature sniffing solution, for primary use within CSS, the error is clearly with the Modernizr team themselves.

It’s all down to the final step outlined in the documentation I quoted earlier:

It adds classes to the html element that explain precisely what features are and are not natively supported.

Modernizr uses JavaScript to add those classes to the html element; one for each of the features detected in the current browser. If we examine the generated source we see the following:

<html lang="en" dir="ltr" id="modernizr-com" class=" js flexbox canvas
canvastext webgl no-touch geolocation postmessage websqldatabase indexeddb
hashchange history draganddrop websockets rgba hsla multiplebgs backgroundsize
borderimage borderradius boxshadow textshadow opacity cssanimations csscolumns
cssgradients cssreflections csstransforms csstransforms3d csstransitions
fontface video audio localstorage sessionstorage webworkers applicationcache
svg inlinesvg smil svgclippaths">

When I first saw that, I was horrified. Not only is it ridiculously cluttered, it also feels decidedly unsemantic and looks like a terminal case of classitis. However, let’s try to be pragmatic here; it’s a means to an end, potentially a minor side-effect to a process that will win us some power in the bigger picture. In fact, it’s one step on from Paul Irish’s proposed work around to the FOUC (flash of unstyled content) which I’ve advocated as a reasonable fallback to marking progressively enhanced objects with an “enhanced” class.

Making use of the classes

The Modernizr method allows you to assign styles based on these new classes on the document’s root element like so (example lifted from the Modernizr docs):

button.glossy {
   background: #ccc url(gloss.png) 50% 50% repeat-x;
}
.cssgradients button.glossy {
   background: #ccc -webkit-gradient(linear, left top, left bottom,
         from(rgba(255,255,255, .4)),
         color-stop(0.5, rgba(255,255,255, .7)),
         color-stop(0.5, rgba(0,0,0, .2)),
         to(rgba(0,0,0, .1)));
}
.cssgradients button.glossy:hover {
   background-color: #fff;
}

Here we can see that button.glossy is first defined with a standard png as its background. The Modernizr-added .cssgradients class is then used to increase specificity and assign a WebKit gradient instead.

What happens if we approach the problem without the .cssgradients class:

CSS allows us to define a property twice in the same rule. It also contains built-in error handling that will ignore a property it doesn’t understand; so browsers that do not understand -webkit-gradient(), for example, will simply ignore a property that uses it. This allows us to progressively enhance our CSS like so:

button.glossy {
   background: #ccc url(gloss.png) 50% 50% repeat-x;
   background: #ccc -webkit-gradient(linear, left top, left bottom,
         from(rgba(255,255,255, .4)),
         color-stop(0.5, rgba(255,255,255, .7)),
         color-stop(0.5, rgba(0,0,0, .2)),
         to(rgba(0,0,0, .1)));
}
button.glossy:hover {
   background-color: #fff;
}

However, because we can no longer target the .cssgradients class specifically, we cannot target the button:hover styles only when CSS gradients are detected. Clearly this is an issue we’ll have to find some way around and is exactly what Modernizr is attempting to solve.

This all seems as if Modernizr is giving us a great deal of power, however, it comes with a significant cost.

Dependency on JavaScript is bad

Fellow web developer and good friend Mike Davies wrote a really great article containing a good summary of obstacles potentially preventing JavaScript from executing back in October 2010. This followed a somewhat presumptuous post by Nicholas Zakas on the YDN regarding the number of users with JavaScript disabled.

In short, there are a large number of obstacles that can affect the execution—or even rewrite the source of—JavaScript included in your pages. Hence, dependancy on JavaScript is bad because it introduces many significant points of failure.

As an example of why dependency on JavaScript can be catastrophic, I recommend you read another of Mike’s posts on Gawker’s JavaScript dependent URIs and how it broke their sites quite considerably. It’s worth noting that the Gawker sites have since seen the error of their ways and are attempting to fix the damage.

Styling dependant on JavaScript

By adding styles that are specific to classes that have been inserted with JavaScript, we are making those styles dependant on JavaScript. This means those styles will fail to apply if JavaScript itself has been disabled or, more commonly, the JavaScript file applying the classes has failed to load or execute successfully due to one of the reasons above. In essence, we may just as well add those styles with JavaScript directly because we’ve already broken the web standards separation of concerns.

Do we really want to remove the majority of CSS items covered by Modernizr if the Modernizr JavaScript include fails for some reason?

It would only be the feature-sniff-dependant CSS rules that would fail, since those classes would now be missing. Potentially this only means the loss of a few nice in-browser rendering optimisations. However, it also means there is a significant loss in the overall structure of specificity within your stylesheets, which could easily cascade unintended effects through to other elements. This could easily result in some content being styled to be unreadable, or worse, incorrectly hidden. This would be very bad indeed.

Alternative solutions

Modernizr is clearly unfit for purpose. If it were simply a JavaScript feature sniffing library for use only in JavaScript, it would be a suitable solution although only really useful for JavaScript progressive enhancement.

With that in mind, what other options are available that allow us to target our CSS?

Server-side browser sniffing

We could browser sniff on the server and add a class to the html element as we generate the HTML that is delivered in the HTTP response. This could result in something like the following:

<html lang="en" dir="ltr" class="firefox">

This means we can target CSS with the .firefox class in much the same way that we used the feature classes that Modernizr provided. There is no way for us to perform feature sniffing on the server; the best we could offer is a feature lookup based on the user-agent string that we’ve used to sniff the browser, but that would ultimately still be browser sniffing.

In fact, the best browser sniffing solution would allow us a little more flexibility on browser versions:

<html lang="en" dir="ltr" class="moz ff ff3-6">

Note I’ve added several classes this time; one for the rendering engine, one for the browser, and one that contains the major and minor versions. This would allow us more specificity.

Update: Matthew Pennell has written an interesting post on server-side browser sniffing with .htaccess and environment variables. Definitely worth a read.

Cache issues

However, server-side browser sniffing means that you’ll run into issues if your pages are publicly cacheable. This server-side solution results in a different HTTP response (the HTML) for each differing browser.

If you utilise any intermediary caching (Squid, Varnish, or a custom origin CDN) for static pages, and you use server-side browser detection, you need to make sure those caches don’t inadvertently send the wrong content to the wrong browser. To do this you’ll need to Vary: User-Agent header in the HTTP response. This instructs any intermediary caches to store multiple copies of the page (one for each User-Agent string that it sees) and to inspect the incoming User-Agent string when looking for cached responses to the current request.

IE targeting with conditional comments

If we’re entirely honest with ourselves as web developers, we can probably admit that the majority of woes we experience in CSS are as a direct result of features that any given version of Internet Explorer has not implemented. In fact, I regularly interview developers who advocate maintaining a separate stylesheet for IE that is included through the use of IE’s conditional comments. Having attempted to use this method in the past, I can confidently say that it is a maintenance nightmare and quickly becomes pretty unmanageable.

A better technique would be to adopt conditional comments to add IE specific classes in much the same way we have with Modernizr or server-side browser sniffing. This method was first proposed by none other than Paul Irish back in 2008 in his article "Conditional stylesheets vs. CSS hacks? Answer: neither". With that method, our html element would look like this:

<!--[if IE ]>
<html lang="en" dir="ltr" class="ie">
<![endif]-->
<!--[if !IE]>-->
<html lang="en" dir="ltr">
<!--<![endif]-->

This looks a bit odd, and often (for the true markup perfectionist) takes a bit of getting used to. However, it does give us what we need without requiring JavaScript or multiple cache versions.

We can even make it more specific if we so desire:

<!--[if lt IE 7 ]>
<html lang="en" dir="ltr" class="ie ie6">
<![endif]-->
<!--[if IE 7 ]>
<html lang="en" dir="ltr" class="ie ie7">
<![endif]-->
<!--[if IE 8 ]>
<html lang="en" dir="ltr" class="ie ie8">
<![endif]-->
<!--[if IE 9 ]>
<html lang="en" dir="ltr" class="ie ie9">
<![endif]-->
<!--[if gt IE 9]>
<html lang="en" dir="ltr" class="ie">
<![endif]-->
<!--[if !IE]><!-->
<html lang="en" dir="ltr">
<!--<![endif]-->

Quite clearly this gets ever uglier, but the solution is entirely encompassed in the structural layer; we will always have the correct class, even if CSS or JavaScript are unavailable for some reason. We also have the same markup regardless of user-agent string, which means caching is not a problem.

The only flaw with this solution is that we can only target versions of Internet Explorer. No other browser implements conditional comments.

Use CSS properly and make use of the cascade

As previously stated, CSS is designed to cascade. This means the built-in error handling will ignore properties the interpreter does not understand, and we can override rules based on specificity and source order. In most cases, these key features are more than adequate to develop gracefully degrading stylesheets.

Most capable front end developers are already doing this and coping just fine. Add this to the previous IE targeting approach and you’ll find that you do not need to over-engineer a solution.

In summary

So, in summary then, I absolutely cannot recommend implementing Modernizr as a best practice for front end development. It attempts to solve a problem from the wrong direction, and introduces a new potential point of failure. What’s more it breaks the fundamental ethic of web standards; the separation of concerns.

I continue to recommend well crafted, gracefully degrading CSS, backed up by conditional commented classes on the html element for targeting IE.

Modernizr may still be a worthy solution for JavaScript-only feature sniffing, if only it allowed the developer to disable the injection of classes on the html element.