Optimizing content for different browsers: the RIGHT way

From Web Education Community Group
Jump to: navigation, search


If you've done a bit of front-end web development, you're bound to have noticed that not all browsers render all web content in exactly the same way. And neither should they have to: arguably your web content doesn't need to look exactly the same across every browser and device and user might choose to view it on, as long as it still provides a good user experience and gives them access to the content and services required by their current browsing experience.

To get your web content working well on all of the huge, diverse number of different web browsers and web-enabled devices that exist (or even a good proportion of them), you're going to have to do some work, optimizing the layouts and content delivered to each browser, and sometimes even directing certain devices to completely separate web sites. Examples of where such optimization might be required include:

  • Serving different layouts to narrow screen devices (eg mobile phones) and wide screen devices.
  • Serving smaller image and video files (or even alternative content representations) to devices that are on a slower internet connection.
  • Serving cutting edge styling to modern browsers, and alternative styling rules to older browsers that don't support the cutting edge CSS.

There are dozens of techniques available to implement such content, but we don't have space to get anywhere near covering them all in this article. We will provide resources to give you pointers to more information at the end.

This article first focuses on the different mechanisms available to allow us to detect what browser is accessing our content. We'll look at the right and wrong way to do this, and then round it off by showing the different mechanisms available to serve appropriate content to different browsers.

Browser detection is not the way to go!

Ok, so the last paragraph in the introduction is a bit misleading — when you want to serve different content to different browsers, detecting the actual browser type and version itself is the wrong way to go in most cases. It is much better to detect whether the browser supports the technologies your web site is using — referred to as feature detection.

This sounds a bit confusing, so let's investigate further.

A brief history of browser sniffing

These days, a lot of web standards features are supported pretty consistently across browsers, or at least, modern browsers.

We'll touch a bit on older browsers we're still sometimes required to support later in the article. We'll also look later at the fact that really new, cutting edge web standards features (such as parts of CSS3 and HTML5) sometimes don't work the same across modern browsers, and the best way to deal with that.

For now, let's rewind back to the early-to-mid-1990s. Back then, web standards were not supported very consistently at all across browsers, so developers were forced to either only support certain browsers, or fork their code, sending completely different codebases to different browsers. The way this was done back then was browser sniffing, or to be more accurate, UA sniffing.

Every browser has a UA string that you can query to find out what browser it is. This is done in JavaScript using the Navigator object. For example, I'm currently using Opera 12.00 beta. If I run navigator.userAgent in my browser, I get the following returned:

Opera/9.80 (Macintosh; Intel Mac OS X 10.6.8; U; Edition Next; en) Presto/2.10.289 Version/12.00

I can use this to identify the browser as Opera, and then serve code to just Opera as a result.

Back in 1993, NCSA Mosaic was the most popular browser. It was identified by

NCSA_Mosaic/2.0 (Windows 3.1)

This was pretty simple. Then Netscape came along, identified by a UA string of

Mozilla/1.0 (Win3.1)

This was also pretty simple, but here's how the problems started. Netscape supported a new killer web feature of the time, Frames, and Mosaic didn't. So web developers used browser sniffing to detect the browser being used and serve content in frames only to Netscape.

Internet Explorer then came along, and also supported frames, but to make sure it was being served frames, Microsoft used the following UA string to identify itself as Mozilla (compatible):

Mozilla/1.22 (compatible; MSIE 2.0; Windows 95)

This is pretty silly, but things got even sillier. Other browser came along later on, with different rendering engines and names, but they all had to keep the Mozilla string inside their UA string, to make sure they were served good content by all this browser sniffing code. For example:

  • Mozilla: Mozilla/5.0 (Windows; U; Windows NT 5.0; en-US; rv:1.1) Gecko/20020826
  • Firefox: Mozilla/5.0 (Windows; U; Windows NT 5.1; sv-SE; rv:1.7.5) Gecko/20041108 Firefox/1.0
  • Opera 9.5: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; en) Opera 9.51 or Mozilla/5.0 (Windows NT 6.0; U; en; rv:1.8.1) Gecko/20061208 Firefox/2.0.0 Opera 9.51 or Opera/9.51 (Windows NT 5.1; U; en)!!!
  • KHTML: Mozilla/5.0 (compatible; Konqueror/3.2; FreeBSD) (KHTML, like Gecko)
  • Safari: Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/85.7 (KHTML, like Gecko) Safari/85.5
  • Chrome: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.13 (KHTML, like Gecko) Chrome/ Safari/525.13

So basically, all these user agent strings are trying to include some kind of identification of the actual browser they belong to and device install on, while strategically trying to be matched by browser sniffing code to make sure the browsers get served decent code.

The problem with browser sniffing

I'm sure you'll agree that the above situation is silly. All of these crazy verbose UA strings have arisen because web developers used short-sighted browser sniffing in the first place, which just sniffed for the first browser to support frames (and later other cool features). When new browsers came out, they had to change their UA strings to work with the browser sniffing code. The other choice would be for developers to keep changing their sniffing code every time a new browser comes out that they want to support. This has happened frequently as well.

and this is the crux of why browser sniffing is so bad — it only solves the problem now, and isn't future proof at all. It is also really error prone. When you are browser sniffing, what you are really want to do is check whether the browser accessing your site supports the technologies your content is built from. But instead of doing this directly, you are making an educated guess dependant on the contents of the UA string, which isn't very precise at all!

For example, you'll notice that the above Opera UA string starts with Opera/9.80, even though at the time of writing we are on version 11.64! This is because of widespread erroneous browsing sniffing code, which looked at Opera UA strings and interpreted Opera 10+ as Opera 1, serving incorrect content as a result.

Why feature detection is better

Feature detection is a much better way to do things — instead of seeing what browser is accessing the content and serving appropriate code, the idea here is to query the browser to detect whether it supports the features our content relies on, and then serve content as appropriate. Let's take HTML5 video as an example. You could detect support using some simple code such as this:

if(!!document.createElement('video').canPlayType === true) {
  // run some code that relies on HTML5 video
} else {
  // do something else

This is much more future proof, because existing browsers that support HTML5 video will run the correct code, and future browsers that support HTML5 video will do as well. You don't have to keep updating your code each time a new browser is released.

Strategies for supporting different browsers

Now we've got the argument for feature detection out of the way, let's look at some strategies you can use to serve appropriate content to different browsers.

Graceful degradation

The good news about web technologies is that they are largely designed so that they will work as well as possible if something is not done quite right — this is contrary to other programming environments where nothing will work if there is the slightest error present in the code.

Part of this related to how browsers deal with unknown elements and CSS properties. If a browser encounters an unknown CSS property, it will just ignore it and move on. If a browser encounters an unknown HTML element, it will treat it like an anonymous inline element, similar to a <span>. This means that if a browser is served some HTML or CSS features it can't understand, it will generally just ignore it and move on, rendering the rest of the content. Obviously this might not give you a usable result in all cases, but for text and image content, you'll find that older browsers will render a usable result, even if it doesn't look quite as nice and shiny as in modern browsers. This is generally called graceful degradation.

A quick example:

border-radius and box-shadow aren't supported in older browsers like IE6, but you're still going to be able to read the content inside the box, even if it hasn't got rounded corners and a drop shadow.


Vendor prefixes, and alternative styles

Of course, you can't rely on graceful degradation in all cases. In some cases, a feature not being supported will result in your content breaking to the point where it is not really usable. In the case of CSS, examples include:

1. Sizing a layout using rem units — these units are not supported in older browsers, resulting in the layout falling apart. To remedy this, you could provide a fallback sized in a unit like pixels later in the cascade, like this:

nav {
  width: 180px;
  width: 18rem;

Modern browsers that understand both will first apply the pixel value then override it with the rem value. Older browsers that don't understand rems will just apply the first line, then ignore the second one.

2. Filling in a background using a CSS3 gradient — in browsers that don't support the gradient, it will not render at all, which could cause content to be unreadable.


To make things better, you could provide a solid background colour as a fallback, or a gradient slice image, like so:

nav {
  background-color: red;
  background-image: url(gradient-slice.png);
  background-image: linear-gradient(top right, #A60000, #FFFFFF);


IE conditional comments

If you are specifically dealing with fallbacks for old versions of Internet Explorer, you can separate these out into separate stylesheets, and then link to these stylesheets inside an IE conditional comment. For example:

<link rel="stylesheet" href="normal-styles.css" type="text/css" />
<!--[if lte IE 6]>
<link rel="stylesheet"  href="ie-fixes.css" type="text/css" />

In this case, the first link element will be given to all browsers, whereas the second one will only be picked up by IE versions 6 and less. In the latter, you could put all your IE fallback styles (such as giving IE6 different width and height values to compensate for its broken box model.)

You can read more about IE conditional comments in Bruce Lawson's article Supporting IE with conditional comments.

Vendor prefixes

Returning to the gradient example we saw earlier, you may have noticed that something wasn't quite right there. To get the gradient rendering across all modern browsers, you need to include multiple versions of the declaration, like this:

nav {
  background-color: red;
  background-image: url(gradient-slice.png);
  background-image: -webkit-linear-gradient(top right, #A60000, #FFFFFF);
  background-image: -moz-linear-gradient(top right, #A60000, #FFFFFF);
  background-image: -ms-linear-gradient(top right, #A60000, #FFFFFF);
  background-image: -o-linear-gradient(top right, #A60000, #FFFFFF);
  background-image: linear-gradient(top right, #A60000, #FFFFFF);

This is because the CSS Image Values and Replaced Content Module Level 3 — the spec that CSS gradients are specified in, is not finalised. Unfinished CSS features are implemented in browsers in an experimental capacity, with a vendor-specific prefix so that different browser's implementations can be tested without impacting on other browser's implementations. In the above example:

  • The -webkit- prefixed property will work in WebKit-based browsers such as Chrome and Safari
  • The -moz- prefixed property will work in Firefox
  • The -ms- prefixed property will work in Internet Explorer
  • The -o- prefixed property will work in Opera

Official W3C policy states that you shouldn't really use experimental properties in production code, but people do, as they want to make sites look cool and keep on the cutting edge. I don't see a problem with this, but if you want to use vendor prefixed properties in your code, you really should use include all the different prefixes so that as many browsers as possible can use all the features you are implementing.

Notice that an unprefixed version of the property has also been included, at the bottom of the list: this is so that when the CSS feature is finalised, and the prefixes are removed, the code will still work — this is good future proofing practice.

Progressive enhancement

The flip side of graceful degradation is progressive enhancement - this goes in the other direction. Instead of building a great experience as the default and then hoping it degrades to something that is still usable in less capable browsers, you build a basic experience that works in all browsers, and then layer an enhanced experience on top of that.



Another approach you can take to dealing with disparate browser support is using Polyfills/shims. These are bits of utility code (usually JavaScript) that fakes support for a feature you want to use in browsers that don't support that feature natively.

You generally use a polyfill by simply applying the JavaScript to your page via <script src="script.js"></script>, although many of them do have other instructions besides. Always reach the author's instructions carefully before attempting to use one! Polyfill/shim examples include:

  • respond.js for making Media Queries work in old versions of IE
  • CSS3PIE for making bling like rounded corners and drop shadows work in old versions of IE
  • Playr for making HTML5 video <track> work in non-supporting browsers
  • HTML5 shiv for making old versions of IE render HTML5 semantic elements properly
  • Excanvas for making Interner Explorer support HTML5 <canvas>

You might think this sounds wonderful! Why worry about what feature different browser support, if you can just fill in support for the ones that don't using JavaScript? The trouble is that using a Polyfill does add weight to a page, both in terms of extra files requiring download, and extra processing required to put the polyfill in action. If you used loads of polyfills on every one of your pages, they would likely be significantly slower to download and run, as well as making your code base a lot more fiddly and complex.

So they are a great idea, but use them sparingly.

Feature detection with Modernizr

You might be thinking at this point "well isn't there a way to only load those extra resources if the browser really needs them?" Well, yes there is. The general idea is to do feature detection to see if the browser supports those features, and then pull in a polyfill or un some alternative code only if needed.

The easiest way to do the feature detection part of this is through a feature detection library called Modernizr. This is a JavaScript library that provides an easy way to do pretty much all of the feature detection you could need. All you have to do is

  1. Download the library and attach it to your page via a <script> element.
  2. add a class of no-js to your page's <html> element.

When your page runs, Modernizr runs feature tests on all manner of HTML5/CSS3/etc. features, then it does two things. First, it adds classes to your <html> element to allow you to apply CSS to the page selectively, depending on whether the browser supports various feature or not. Second, it provides you with a JavaScript API to allow you to do the same kind of selective code application in your JavaScript. Let's look at these two aspects in more detail.

Selective CSS application

To get things going, you need to link to the Modernizr library and add a class of no-js to the <html> element in your source code. when the page is run, modernizr runs all its feature detection tests and lets you know the results of those tests via a series of classes appended to the <html> element. They will look something like this:

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

This is really powerful, as it allows you to apply CSS to the page selectively, depending whether it supports certain feature or not. For example, if your code uses an animated 3D transform by default to reveal some content underneath:

#shutter:hover {
  transform: rotateX(90deg);

You can include alternative styling that overrides this in browsers that don't support 3D transforms like this:

.no-csstransforms3d #shutter:hover {
  position: relative;
  right: 100px;

It might not look quite as elegant, but it would still produce an ok experience that allows access to the underlying content.

Selective JavaScript application

Modernizr also provides a nice easy JavaScript API to allow you to execute code selectively, depending on whether CSS/HTML/other features are supported by the browser. Each test is a simple method on the Modernizr object, which returns a boolean (true/false) result. For example, if you wanted to query whether the current browser supports 3D transforms in JavaScript, you would write if(Modernizr.csstransforms3d). So you could have a block of code like this:

if(Modernizr.csstransforms3d) {
  // run killer feature that relies on 3D transforms being available
} else {
  // run an alternative chunk of code that provides a graceful fallback

Problems with Modernizr

Modernizr, as with everything, isn't perfect. Two of the main problems are:

  1. Not everything can be feature detected and therefore dealt with in this elegant manner.
  2. The full Modernizr library weighs in at 49KB, which is quite a lot of extra code to download for a few feature tests.

However, you can customize Modernizr and create a version suitable for your site that only contains the tests you need, which cuts down on the file weight. And even though you can't feature detect everything, you will find it invaluable in many situations.

YepNope - load only what you need!

Now we've covered the feature detection side of things, lets cover what we can do to load resources like Polyfills only when you need them. To help you out with this, Modernizr comes packaged with a really smart, dandy little utility library called YepNope. This can do a lot, but in a nutshell, it allows you to specify a test of some kind, and then say what happens when the test is passed, and when it fails. Here's a simple example:

  test : Modernizr.canvas,
  yep  : 'normal.js',
  nope : ['polyfill.js', 'normal.js']

So basically, you are sating what the test is, then saying that if the test is passed, just load the normal script, but if the test fails, load the normal script and a polyfill to allow the code to work in an older browser.

You can find a lot more details on how to use YepNope at The YepNope homepage.


I hope that's helped you get all these ideas clear in your head — why browser sniffing is bad, why feature detection is a much better way to detect whether a browser will run your site features or not, and different strategies for providing different capability browsers with different but acceptable experiences.

If you want to read more sources about the topics discussed above, please check out the following pages: