An unexcited look at browser sniffing

July 08, 2012

Do you use browser sniffing? Oh, you evil person! Your poor soul will rot and burn forever, you're summoning the wrath of the heavens!

Do you use feature detection exclusively? Aah, a pure spirit! Enlightenment has come to you; you shall forever be applauded by the angels above!

Do you live in the real world? Then this post may be for you.

Not all sniffing is created equal

Not only is any black-and-white view on the whole feature detection vs. browser sniffing issue oversimplifying and not helpful; even the idea that you could just point a finger at a piece of code and say “this part is browser sniffing, that part is not” is a fallacy to begin with. Sometimes the distinction may be clear, but sometimes it just may not.

Let's look at a few techniques that could be called browser sniffing.

Examining the user agent string

The classic method of browser sniffing. Is the user browsing your site with Internet Explorer? Just check if the user agent string contains “MSIE”. You can do this both on the server (which receives the user agent as an HTTP header) or on the client via JavaScript (by looking at navigator.userAgent).

For Internet Explorer this is fairly reliable; for other browsers, the check may be harder. Just because the user agent contains “Safari” doesn't mean it's Safari; it could just as well be Chrome. And of course the presence of the string “Mozilla” in the user agent tells you nothing; least of all whether the browser has anything to do with Mozilla.

This is usually a “last resort” sort of thing; if you can check anything else that's more specific to your use case, you should.

Conditional comments and browser hacks

Conditional comments are an IE-specific solution that's usually used for applying CSS workarounds for certain Internet Explorer versions, but can be used to hide any HTML you want from other browsers and from IE versions to which your particular fix doesn't apply.

I'll let you smile for a moment at this introductory text from MSDN's page on conditional comments:

Conditional comments make it easy for developers to take advantage of the enhanced features offered by Microsoft Internet Explorer 5 and later versions, while writing pages that downgrade gracefully in less-capable browsers or display correctly in browsers other than Windows Internet Explorer. Conditional comments are the preferred means of differentiating Cascading Style Sheets (CSS) rules intended for specific versions of Internet Explorer.

Oh, the good ol' times…

Anyway, I'm personally not a fan of conditional comments, since they have to be included in the HTML page, while most of the time, they apply to your style sheets, which should be static, long-cached documents from a cookie-free domain. But conditional comments will always be included in your (probably dynamic) HTML pages, even for the majority of people who have moved on from IE7.

This is a bit of personal preference though. It's an officially-supported way to work with IE, so feel free to use it if you must.

Browser hacks are somewhat similar to this, but they are specific to CSS, not necessarily specific to IE, and usually rely on creating CSS rules that are considered broken (and thus ignored) in all browsers except the targetted one. Paul Irish and Mathias Bynens have some details on conditional comments and browser hacks.

Either way, sometimes you don't get around browser-specific CSS rules, so if all else fails, a little \9 here and there is not going to get you imprisoned.

Vendor prefixes

Hardly ever mentioned in the context of browser sniffing, but in my view it's part of the story. Only that it's specifically sanctioned by the browser makers.

background: -webkit-gradient(linear, left top, left bottom, from(black), to(white));
background: -moz-linear-gradient(top, black, white);

Okay; Webkit and Gecko disagree on the implementation of gradients, so we have to do if Webkit then X, else if Gecko then Y. Probably a good thing in that particular case, since they are incompatible. If you want, you can consider this feature detection instead; after all, you are checking whether the browser supports -moz-linear-gradient, and if so, you use it.

No matter what you call it, unfortunately this kind of check doesn't let you take shortcuts, even when you know that things work identical in all browsers:

-webkit-animation-name: bubble;
-webkit-animation-iteration-count: infinite;
-webkit-animation-timing-function: linear;
-moz-animation-name: bubble;
-moz-animation-iteration-count: infinite;
-moz-animation-timing-function: linear;
-o-animation-name: bubble;
-o-animation-iteration-count: infinite;
-o-animation-timing-function: linear;

– and with IE 10, a fourth kind will have to be added (but at least it's the prefix-less version, not -ms-). Not to mention that you also have to specify the animation keyframes separately via @-webkit-keyframes bubble {...}, @-moz-keyframes bubble {...} and so on.

So there's an example where you're forced to do browser sniffing, even though you don't even have to, let alone want to.

Any similarities between Opera's announcement to support -webkit- prefixes and the above-mentioned “Mozilla in every user agent string” situation are of course purely coincidental.

Checking for distinctive features

Quiz: Is this browser sniffing or feature detection?

if (document.attachEvent)
    // do IE-specific stuff
else
    // do stuff for other browsers

The correct answer is “It depends.” Namely on what the “IE-specific stuff” is.

The following is browser sniffing, because it uses the presence of document.attachEvent as a way to detect that we're using Internet Explorer, and applies CSS settings accordingly (which by itself of course has nothing to do with attachEvent):

if (document.attachEvent)
    myElement.style.filter = "alpha(opacity=50)";
else
    myElement.style.opacity = .5;

This of course is something you should usually avoid if you can; it's really no different from checking the user agent string for “MSIE”. In this particular case, the better way would be to use if ("filter" in document.body.style) instead.

What follows here is feature detection, because it checks for the presence of an API, and if that API is available, it's used:

if (document.attachEvent)
    document.attachEvent("onclick", myHandler);
else
    document.addEventListener("click", myHandler, false)

It can be argued that it's better to check for presence of the standard API first, and only then fall back to the IE-specific one:

if (document.addEventListener)
    document.addEventListener("click", myHandler, false)
else
    document.attachEvent("onclick", myHandler);

if ("opacity" in myElement.style)
    myElement.style.opacity = .5;
else
    myElement.style.filter = "alpha(opacity=50)";

The above will use the standard event handler method and the standard CSS property in IE 9 and later, which supports both the IE-specific and the standard versions.

But I consider checking for IE versions first just as fine; if the browser still supports the old API, it's reasonable to expect that we can use it. When Microsoft decides to drop them in a later version, the code will still work.

Keep it clean, at least if you can

Of course, browser sniffing should be used in the smallest possible amount and avoided where reasonably possible. That's why the jQuery project will remove $.browser from jQuery 1.9, saying

Ever since jQuery 1.4, we’ve been evangelizing that browser detection via the user agent string is a bad idea. Yet we’ve been an enabler of bad practice by continuing to offer $.browser. As of jQuery 1.9 we’ll remove it entirely and you’ll need to use the 1.9 compat plugin. If your code isn’t weaned off browser detection yet, check out Modernizr for a very thorough set of feature detections you can use instead. And of course, you’re welcome to read the tea leaves in the navigator.userAgent string directly, there’s nothing stopping you but your conscience.

But sometimes, you have no choice; sometimes, you're positively obliged to do browser sniffing, and sometimes, it's not your conscience, but practicality that stops you from doing it – and sometimes, plain old human emotion.

Here are a few real-life examples of browser bug workarounds.

Checking for the bug, not for the browser

First, let's look at an issue that caused problems on LaTeX-enabled Stack Exchange sites. Safari incorrectly implements the JavaScript String.prototype.replace method. You can call this method with either a string or a function as the second argument:

alert("Hello people".replace("people", "world"));
alert("Hello people".replace("people", function () { return "world"; }));

These two lines do the same thing. This changes, however, when the replacement string contains the dollar character. The following two lines do different things:

alert("Give me dollars.".replace("dollars", "$$"));
alert("Give me dollars.".replace("dollars", function () { return "$$"; }));

The first line alerts “Give me $.”, the second line says “Give me $$.” This is because when the second argument to replace is a string, the dollar sign has a special meaning in this string, but if it's a function, its return value is used varbatim as the replacement string without any special handling. This is set forth in Section 15.5.4.11 of the ECMAScript specification.

Enter Safari, in which the two lines give the same result “Give me $.”

Safari makes the replacement string subject to the special dollar handling even if it was the return value of a replacement function (interestingly, this only happens if the search value, the first argument, is a string, but not when it's a regular expression).

So how would you work around this browser bug? You could do this (Safari check from quirksmode):

var isSafari = /Apple/.test(navigator.vendor);
if (isSafari)
    alert("Give me dollars.".replace("dollars", function () { return "$$$$"; }));
else
    alert("Give me dollars.".replace("dollars", function () { return "$$"; }));

It's a pragmatic solution. I have found no other browser where this bug occurs, and it seems to occur in all versions of Safari, so browser sniffing will fix your problem. But there's a better way, and it's just as easy – don't check for the browser, instead check for the bug:

var dollarHandlingIsBroken = "x".replace("x", function () { return "$$"; }).length === 1;
if (dollarHandlingIsBroken)
    alert("Give me dollars.".replace("dollars", function () { return "$$$$"; }));
else
    alert("Give me dollars.".replace("dollars", function () { return "$$"; }));

Suddenly, you have a way to work around the bug which will still cause the correct behavior if Apple fixes the bug, and also will work in browsers that have the same bug but are not (or don't identify themselves as) Safari.

Doing the bad thing because it's the right thing

Sometimes it's impossible to check for the presence of an issue; all you know is that at least some version of some browser has this bug. I have listed a few examples of such bugs in this answer on Meta Stack Overflow. In a case like this, you should keep two things in mind:

First, your workaround should, wherever possible, not break in browsers that don't have the issue. If your site breaks in a browser that you want to support, you have no choice but to put in some sort of workaround. But maybe the browser gets an update with a bugfix, or maybe the issue doesn't appear on all platforms. And since you can't check for the presence of the bug itself, you won't know.

Second, your workaround may be less performant than the version for non-broken browsers, or it may remove some nice-to-have features, or it may cause a startup delay, or it may cause some other form of degraded user experience. If there's no other way, then so be it. But in such a case by all means use browser sniffing. Don't let anyone tell you that you should never do that, just because.

You're doing your users a favor here. If you know for sure that the bug only exists in some builds of Firefox 12 on Mac, but nowhere else, and your workaround requires an extra confirmation click, then sniff as much as you can! If you know the bug doesn't exist in Firefox 12 on Linux, you shouldn't force the users through an extra confirmation just because browser sniffing is bad.

Doing the good thing for your pride

Pragmatic solutions shouldn't be underestimated, and usually you just want to get things done. But once in a while, say to yourself “What if I don't put an if IE check here, but try to come up with a cleaner solution?” It may just be worth a bit of time, both as a learning exercise, and as a way to feel good.

Let's say you find a nice little trick to work around a browser bug without resorting to browser hacks, or conditional comments, UA string checks, or similar. Of course a clean workaround is always preferable, but there's an advantage that goes beyond just being as standards-conform as possible: Having found such a way will make you feel proud. I'm not kidding; finding a clean hack will make your day. It's a feeling of victory. You haven't given in to IE, you have conquered it without sacrificing your ideal of wanting to write unsoiled code.

Maybe you find this ridiculous, but for me personally, patting myself on the back because I'm proud of a solution I found is part of what makes my job enjoyable.

I'll give you an example from the CSS of Arqade, a.k.a. Gaming Stack Exchange. You may find this utterly boring, not worth mentioning, totally obvious, or unbelievably pretentious, and I'll live with that. Just believe me that on the day I've found this workaround, I was proud.

An element was supposed to have multiple background images. Easy enough:

background-image: url('a.gif'), url('b.png');

Not all browsers support multiple backgrounds (well, IE 8 doesn't, that's about it, as far as moderately non-ancient browsers go). But that's okay, the top image (a.gif) is the most important one; b.png is a nice-to-have. So add a fallback for browsers without support for multiple background images:

background-image: url('a.gif');
background-image: url('a.gif'), url('b.png');

In browsers that support multiple backgrounds, the second rule will overwrite the first, and all is well. In browsers that don't, the second rule is illegal and thus ignored, and the first one ends up counting. All good, right? Enter IE 8, which doesn't ignore the second rule like it should, but instead reads it like this:

background-image: url('a.gif');
background-image: url('a.gif%29,%20url%28%27b.png');

That looks like a valid rule, so it overwrites the first, but of course there is no image file with that weird name. Ergo the element suddenly has no background at all.

A browser hack would have done the trick here – just create an extra rule for IE 8 only. But I didn't want to do that, so I tried to find a better way, and eventually I did.

The solution: Since the images are static files, the web server will ignore any query string, a fact that is used a lot for cache breaker functionality. Here, we use it to trick IE 8 instead:

background-image: url('a.gif?'), url('b.png');

IE 8 will download the image file a.gif?%29,%20url%28%27b.png, which is just a.gif with a query string, and use it as the background image. All other browsers download both a.gif? (again, a.gif with a query string, this time empty) and b.gif, and display the two as stacked background images.

If you don't understand why I was proud after having found this solution, that's okay; but if you do, maybe it motivates you to try finding a clean solution to your next browser bug.


previous post: JavaScript concurrency and locking the HTML5 localStorage

next post: John Resig: Secrets of the JavaScript Ninja

blog comments powered by Disqus