October 2011

Volume 26 Number 10

HTML5 - Browser and Feature Detection

By Sascha P. Corti | October 2011

If you’re building a Web site, you don’t just want it to look terrific today; you want it to dazzle for a long time to come. That means your site has to work not only in today’s browsers, but also in future versions. In this article I’ll present some tips and best practices that will help you achieve this goal.

A Bit of History

Today, all Web browsers are built with one common goal: Render Web pages optimally according to the latest specifications.

This wasn’t always the case. In the past, as browser vendors raced to become dominant, most implemented the features that were in high demand, even if they weren’t yet standardized. Of course, each browser did it in its own way. Here’s an example of how setting transparency in CSS varied.

Internet Explorer before version 8 understood the following CSS:

.transparent {      /* Internet Explorer < 9 */
    width: 100%;
    filter:alpha(opacity=50);
}

while Firefox had its own attribute:

.transparent {
    /* Firefox < 0.9 */
    -moz-opacity:0.5;
}

as did Safari:

.transparent {
    /* Safari < 2 */
    -khtml-opacity: 0.5;
}

Now, however, in CSS3, there’s a unified way to set an element’s transparency:

.transparent {
    /* IE >= 9, Firefox >= 0.9, Safari >= 2, Chrome, Opera >= 9 */
    opacity: 0.5;
}

While it might seem good for a browser to go the extra mile to support nonstandard features, it makes the life of a Web developer harder than need be, because he has to take all the various implementations of the feature into consideration when adding it to a page.

Consistent Markup

The best way to make sure your Web page will render optimally in all browsers is to focus on markup that’s certain to be supported in all current versions of the browsers. Until very recently, this was HTML 4.01, a 10-year-old standard with very limited features.

Today, all browsers are converging toward the feature-rich HTML5—but many of the new specifications summarized under that general term, including HTML5 markup, its APIs such as DOM Levels 2 and 3, CSS3, SVG and EcmaScript 262, are still in development and thus subject to change. The browser vendors are continually adding support for new HTML5 features, but at quite different paces.

Firefox and Opera are usually very quick to adopt new HTML5 specifications, sometimes even those in early development and subject to change or that have security issues. While this can be interesting for developers to test new features, it can lead to Web pages that break between browser releases because of drastic changes between a specification and its implementation. That’s a frustrating experience for both users and developers. An example of this was Firefox 4 disabling Websockets between Beta 7 and 8 due to security reasons.

Chrome, which is also very fast to adopt new HTML5 standards, recently stirred the HTML5 community with the announcement that it was abandoning support for the popular h.264 video codec for HTML5 <video> elements and instead switching to the royalty-free WEBM standard. Though this may be good for developers who currently pay for h.264 licenses, it adds another choice that developers will have to keep track of in order to support as many browsers as possible.

Microsoft is slower to implement standards, but it but works closely with the W3C to build test suites, helping to minimize ambiguity in the specifications and building a technical foundation to help vendors work on homogenizing the way their browsers render HTML5. To see the latest work in this area, have a look at the Internet Explorer 10 Platform Preview, which you can find on IE Test Drive. You’ll also want to check out the HTML5labs where Microsoft prototypes early, unstable specifications from Web standards bodies such as the W3C. See the section, “Improved Interoperability Through Standards Support,” of the Internet Explorer 9 Guide for Developers for in-depth information on how Internet Explorer 9 supports the various HTML5 specifications today.

Still, because the new HTML5 standards remain a moving target, and because most Internet users don’t use the latest versions of the various Web browsers, serving the right markup is more important than ever.

Browser Detection

One approach to handling differences among browsers is to use browser detection. The most common way to do this is to use JavaScript to query the user-agent header:

<script type="text/javascript">
  if ( navigator.userAgent.indexOf("MSIE")>0 )
  {
    // Run custom code for Internet Explorer.
  }
</script>

The problem with this approach is twofold. First, it bundles multiple assumptions about the features the browser supports in one check. A single wrong assumption can break the site. So as a developer you have to keep track of exactly which features each version of a specific browser supports.

The second issue is that this browser check doesn’t take browser versions into consideration and therefore isn’t future-proof. Even if it works with today’s version of a browser, the next release might not require—or worse, might remove support altogether for—a workaround that the browser detection was used to add to the site.

Therefore, if you have to use browser detection, make sure you take the version into consideration and only use this approach to detect legacy browsers, as shown in Figure 1.

Figure 1 Detecting Legacy Browsers

<script type="text/javascript">
  functiongetInternetExplorerVersion()
  // Returns the version of Internet Explorer or a -1 for other browsers.
  {
    var rv = -1;
    if(navigator.appName == 'Microsoft Internet Explorer')
    {
      var ua = navigator.userAgent;
      varre  = newRegExp("MSIE ([0-9]{1,}[\.0-9]{0,})");
      if(re.exec(ua) != null)
      rv = parseFloat( RegExp.$1 );
    }
    return rv;
  }
  functiononLoad()
  {
    var version = GetInternetExplorerVersion()
    if (version <= 7 && version > -1)
    {
      // Code to run in Internet Explorer 7 or earlier.
    }
  }
</script>

The MSDN Library page, “Detecting Browsers More Effectively,” has more information, and you’ll find a comprehensive article on how to use the navigator object and regular expressions to detect the various browsers and their exact versions at JavaScript Tutorials.

Starting with version 5, Internet Explorer has a unique way to detect browsers using conditional comments. This syntax extends the standard HTML comments. You can use these conditional comments with a CSS to implement certain Internet Explorer-specific CSS rules that you want other browsers to ignore. In the following sample, “ie7.css” is loaded only if Internet Explorer 7 or earlier is detected:

<!--[if lte IE 7]>
  <style TYPE="text/css">
    @import url(ie7.css);
  </style>
<![endif]-->

Detailed information on how to use conditional comments can be found in the MSDN Library page, “About Conditional Comments.”

Given all the problems and limitations of browser detection, however, let’s look at an alternative.

Feature Detection

A far better approach to handling differences among Web browsers is to use feature detection. Before using a feature that you know has different implementations in the various browsers, you run a small test that looks for the availability of a specific object, method, property or behavior. In most cases this can be done by trying to create a new instance of the feature in question and if that instantiation returns something other than null, the executing browser knows this feature. If not, you can follow up by testing for the availability of a workaround or a proprietary legacy implementation of the feature.

Comparing Browser and Feature Detection

Let’s use the diagrams in Figures 2, 3 and 4 to help visualize how the two approaches work in various situations.

Possible Code Paths Through the Test Site
Figure 2 Possible Code Paths Through the Test Site

Results with Well-Known Browser Configurations
Figure 3 Results with Well-Known Browser Configurations

When faced with well-known browser configurations, both methods work, but browser detection has the fixed assumption that both Feature A and Feature B are supported by the browser, whereas feature detection tests for each feature individually.

Unknown Browser Configuration
Figure 4 Unknown Browser Configuration

It’s when faced with an unknown browser configuration that things get interesting. Feature detection handles this well and finds out that the browser is capable of displaying Feature A but needs fallback code for Feature B. Browser detection, on the other hand, picks a path based on the browser’s name or chooses the default path because none of the queried browser/version combinations match. Either way, in this example the page won’t render properly because there’s no code path that connects all the valid code segments even though the page actually contains all the code necessary to properly display in this unknown browser configuration.

Feature Detection Examples

There are two very important recommendations to keep in mind when using feature detection:

  • Always test for standards first because browsers often support the newer standard as well as the legacy workaround.
  • Always target only related features in a single check, effectively minimizing assumptions of a browser’s capabilities.

Now let’s look at a few examples of feature detection.

The following script creates two code paths. It first checks if the browser supports window.addEventListener and if not, probes for the availability of the legacy feature window.attachEvent:

<script type="text/javascript">
  if(window.addEventListener) {
    // Browser supports "addEventListener"
    window.addEventListener("load", myFunction, false);
  } else if(window.attachEvent) {
    // Browser supports "attachEvent"
    window.attachEvent("onload", myFunction);
  }
</script>

Another good approach is to encapsulate feature detection into a set of functions that can then be used throughout the code. Here’s a best practice for detecting whether the browser supports the HTML5 <canvas> element and if so, makes sure that the canvas.getContext(‘2d’) method is working as well. This function simply returns true or false, making it easy to reuse.

<script type="text/javascript">
  functionisCanvasSupported()
  {
    var elem = document.createElement('canvas'); 
    return!!(elem.getContext && elem.getContext('2d');
  }
</script>

One thing to keep in mind when using feature detection is to always use it on newly created elements or objects so you avoid the possibility that any other script on the page has modified the element or object since it was created, which could lead to random or erratic results.

Feature detection also works directly for a few HTML elements, such as HTML5 <video>, <audio>, and <canvas> in the form of a “fallback.” The browser displays the first supported sub-element from the top and visually hides the elements below.

The easiest form looks like this:

<video src="video.mp4">
    Your browser doesn't support videos natively.
</video>

A browser that supports the <video> element will show the video “video.mp4” while a browser that doesn’t will fall back to the supplied text. But fallback also works for various video formats in the video tag:

<video>
    <source src="video.mp4" type="video/mp4" />
    <source src="video.webm" type="video/webm" />
    Your browser doen't suppport videos natively.
</video>

In this case, a browser that supports HTML5 <video> will first try to load the mp4 video. If it doesn’t support this format, it will fall back to the webm-encoded video. Should that format not be supported or should the browser not support <video> at all, it will fall back to the text.

Of course, it makes more sense to fall back to a plug-in video player instead of the text, in case the browser doesn’t support HTML5 <video> at all. The following sample uses a Silverlight video player:

<video>
    <source src="video.mp4" type='video/mp4' />
    <source src="video.webm" type='video/webm' />
    <object type="application/x-silverlight-2">
        <param name="source" value="https://url/player.xap">
        <param name="initParams" value="m=https://url/video.mp4">
    </object>
    Download the video <a href="video.mp4">here</a>.
</video>

A very similar logic works in CSS as well. In CSS, unrecognized properties are simply ignored. So when you want to add multiple experimental, vendor-prefixed properties, as shown with “border-radius” below, you can simply include all the variations in your code. This may feel a little imprecise, but it’s easy to use and gets the job done for cases like this. Note that it is a best practice to put the vendor-specific prefixes you want to include first and the standard markup last.

<style type="text/css">
    .target
    {
        -moz-border-radius: 20px;
        -webkit-border-radius: 20px;
        border-radius: 20px;
    }
</style>

A big upside to feature detection is that it also works with browsers that you weren’t even thinking about when creating your page, and even with future versions of browsers because it doesn’t rely on any assumptions about what features a browser supports.

Developing and Testing Feature Detection

The F12 Developer Tools in Internet Explorer 9 are great for developing and testing feature detection across many browsers. You can use them to debug script step by step and to change the browser’s user agent string, as well as to tell Internet Explorer to use the rendering engine of the previous versions**. Figures 5, 6, 7** and 8 show some examples of how you can use these tools.

Using Breakpoints while Debugging JavaScript in Internet Explorer 9
Figure 5 Using Breakpoints while Debugging JavaScript in Internet Explorer 9

Running in “Document Mode: IE9 standards,” the bBrowser Uses the Modern addEventListener Method
Figure 6 Running in “Document Mode: IE9 standards,” the bBrowser Uses the Modern addEventListener Method

Running in “Document Mode: IE7 standards,” the Debugger Falls Back to the Legacy attachEvent Method
Figure 7 Running in “Document Mode: IE7 standards,” the Debugger Falls Back to the Legacy attachEvent Method

You Can Change the Internet Explorer User Agent String on the Fly and Even Add Your Own Strings, Including Those for Mobile Browsers
Figure 8 You Can Change the Internet Explorer User Agent String on the Fly and Even Add Your Own Strings, Including Those for Mobile Browsers

Managing Feature Detection in Large Projects

When creating a complex Web project, creating and managing all the feature-detection code can be tedious. Luckily, there are great JavaScript libraries available that help in this effort, namely Modernizr and jQuery.

Modernizr has built-in detection for most HTML5 and CSS3 features that’s very easy to use in your code. It’s very widely adopted and constantly enhanced. Both Modernizr and jQuery are shipped with the ASP.NET MVC tools.

Take a look at the code in Figure 9, which detects the browser’s capability to display web fonts without using Modernizr, and then at the code in Figure 10 that does use Modernizr.

Figure 9 Without Modernizr

function(){
  var
    sheet, bool,
    head = docHead || docElement,
    style = document.createElement("style"),
    impl = document.implementation || { hasFeature: function()
      { return false; } };
    style.type = 'text/css';
    head.insertBefore(style, head.firstChild);
    sheet = style.sheet || style.styleSheet;
    var supportAtRule = impl.hasFeature('CSS2', '') ?
      function(rule) {
        if (!(sheet && rule)) return false;
          var result = false;
          try {
            sheet.insertRule(rule, 0);
            result = (/src/i).test(sheet.cssRules[0].cssText);
            sheet.deleteRule(sheet.cssRules.length - 1);
          } catch(e) { }
          return result;
        } :
        function(rule) {
          if (!(sheet && rule)) return false;
          sheet.cssText = rule;
          return sheet.cssText.length !== 0 &&
            (/src/i).test(sheet.cssText) &&
            sheet.cssText
              .replace(/\r+|\n+/g, '')
              .indexOf(rule.split(' ')[0]) === 0;
        };
    bool = supportAtRule('@font-face { font-family: "font";
    src: url(data:,); }');
    head.removeChild(style);
    return bool;
  };

Figure 10 With Modernizr

<script type="text/javascript" src"modernizr.custom.89997.js"></script>
<script type="text/javascript">
  if(Modernizr.fontface){
    // font-face is supported
  }
</script>

Adding Support for Missing Features

Feature detection doesn’t eliminate the burden of coming up with a workaround when the tested browser doesn’t support a feature you need. In the earlier HTML5 video sample, using Silverlight as a fallback was an obvious solution. But what about other HTML5 features, like <canvas> or the new semantic tags, such as <nav>, <section> and <article>, <aside>, or the new <header> and <footer>?

A growing number of ready-made “fallbacks” for many HTML5 features, known as shims and polyfills, can ease that burden. These come in the form of CSS and JavaScript libraries or sometimes even as Flash or Silverlight controls that you can use in your project, adding missing HTML5 features to browsers that don’t otherwise support them.

The general idea is that developers should be able to develop with the HTML5 APIs, and scripts can create the methods and objects that should exist. Developing in this future-proof way means that as users upgrade, the code doesn't have to change and users will move to the better native experience cleanly.

The difference between shims and polyfills is that shims only mimic a feature and each has its own proprietary API, while polyfills emulate both the HTML5 feature itself and its exact API. So, generally speaking, using a polyfill saves you the hassle of having to adopt a proprietary API.

The HTML5 Cross Browser Polyfills collection on github contains a growing list of available shims and polyfills.

Modernizr, for example, includes the “HTML5Shim” for semantic tag support, but it’s easy to load other shims and polyfills if Modernizr detects that a feature isn’t supported.

Adding Support Dynamically

You may be asking yourself, “Won’t adding all these script libraries make my page huge and slow to load?”

Well, it’s true that using many of these supporting libraries can add substantial overhead to your Web site, so it makes sense to load them dynamically only when they’re really needed. In the case of a shim or polyfill, the best practice is to load them only when you’ve detected that the browser doesn’t support the native HTML5 feature.

Again we’re in luck because there’s a great library that supports exactly this scenario: yepnope.js

Yepnope is an asynchronous resource loader that works with both JavaScript and CSS and fully decouples preloading from execution. This means that you have full control of when your resource is executed and you can change the order on the fly. Yepnope will be integrated in Modernizr 2, but can also be used on its own. Let’s have a look at its syntax:

<script type="text/javascript" src="yepnope.1.0.2-min.js"></script>
<script type="text/javascript">
    yepnope({
        test : Modernizr.geolocation,
        yep  : 'normal.js',
        nope : ['polyfill.js', 'wrapper.js']
    });
</script>

This example from the Yepnope page tests the browser’s ability to use HTML5 geolocation using Modernizr. If supported, your own code (normal.js) will be loaded; otherwise, a custom polyfill (that consists of polyfill.js and wrapper.js) will be loaded.

The most important points are summarized in Figure 11.

Figure 11  Browser Detection and Feature Detection Dos and Don’ts

  DO DON’T
Browser Detection

Try to avoid altogether.

or

Test for a specific browser and version.

Make assumptions for future browsers by only testing for the browser name.
Feature Detection Test for standards first. Assume unrelated features under one test.

Resources:

Learn more about browser and feature detection at:


Sascha P. Corti works for Microsoft Switzerland as a developer evangelist focusing on the Microsoft system platform and developer tools, and he introduces the latest trends to the Swiss developer community. He currently focuses on Web technologies and the Windows Phone 7 platform.

His studies involved Computer Science at ETH Zurich and Information Management at the University of Zurich.

For seven years Sascha was a software developer and architect for a major Swiss bank and he has worked as a systems engineer and technology specialist for several American hi-tech companies, including Silicon Graphics and Microsoft.

You can read about his latest activities on his weblogs at https://techpreacher.corti.com and https://blogs.msdn.com/swiss_dpe_team/ or follow him on Twitter @TechPreacher.

*Thanks to the following technical experts for reviewing this article: Justin Garret and *Thomas Lewis