Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Browser and Feature Detection: Make your Website look Great Everywhere

17 Oct 2011 1  
When you’re building a web site, you want it to look great in any browser…and ideally, it should look great for a long time, even in future versions of the browsers. I’ve pulled together some hints, tips and best practices that will help your sites look their best.

This article is in the Product Showcase section for our sponsors at CodeProject. These articles are intended to provide you with information on products and services that we consider useful and of value to developers.

When you’re building a web site, you want it to look great in any browser…and ideally, it should look great for a long time, even in future versions of the browsers. I’ve pulled together some hints, tips and best practices that will help your sites look their best.

A Bit of History

Today, all web browsers are built with one common goal in mind: Render webpages optimally according to the latest specifications.

This wasn’t always so. In the past, most browsers implemented high-demand features that were not yet standardized. Each browser did so in its own way, like, for example, the possibility to set transparency in CSS.

Internet Explorer, before v.8, understood the following CSS:  

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

Firefox had its own attribute:  

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

As did Safari:  

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

Today, however, in CSS3, there is a unified way of setting the transparency of an element:  

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

It can certainly be seen as a good thing when browsers go the "extra mile" to support new features quickly. But when those features aren’t standardized, it definitely makes life harder for developers, since we have to account for all the different implementations of each feature.

Same Markup

The best way to make sure your web page will render optimally in all browsers is to use markup that is supported in all current 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. However, many of the new specifications that fall under the term "HTML5" (including HTML5 markup, its APIs including DOM Levels 2 and 3, CSS3, SVG, and EcmaScript 262) are still in development and subject to change.

Browser vendors are continuously adding support for HTML5 features, but at quite different paces.

Firefox and Opera are usually quick to adopt new HTML5 specifications—even some that are in early development, subject to change, or have security issues. 

On the upside, it's great that developers get a chance to test new features. However, this early adoption rate often leads to sites that use features that break pages between browser releases. An example of this was Firefox 4 disabling Websockets between Beta 7 and 8 due to security reasons. It's an extremely frustrating experience for both users and developers

Chrome—which is also quickly adopting new HTML5 standards—has recently managed to stir the HTML5 community with its announcement to abandon support for the popular h.264 video codec for HTML5 <video> elements and instead switch to the royalty free WEBM standard. While this is not a bad decision for developers that currently pay for h.264 licenses, it again adds more choices that developers will have to keep track on what video formats they will need to encode and host their content in order to support as many browsers as possible.

Microsoft doesn’t implement standards as quickly, but does work closely with the W3C to build test suites. This collaboration helps to:

  • Minimize ambiguity in the specifications
  • Build technical foundations for browser vendors to work on
  • And homogenize how browsers render HTML5. 

To see their latest work in this area, have a look at Internet Explorer 10 Platform Preview that’s available on IE Test Drive.

You can also check out the HTML5labs, where Microsoft prototypes early and unstable specifications from web standards bodies such as the W3C. See Improved Interoperability Through Standards Support for in-depth information on how Internet Explorer 9 supports the various HTML5 specifications today.

But since the new HTML5 standards are still a moving target—and most Internet users don’t use the latest version of any given browser—serving the right markup is more important than ever.

Browser Detection

One approach to cover browser differences is by using browser detection. The most common approach 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>

But there are a couple of problems with doing it this way.

First, it bundles multiple assumptions of which features the browser supports in one single check. A single wrong assumption can break the site. So, as a developer, you have to keep track of which features are supported by which browsers.

The second issue is that the browser check above doesn’t take browser versions into consideration, so it isn’t future-proof. So 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.

If you have to use browser detection, make sure to only use it to detect legacy browsers as shown in the following example, and always keep in mind the version of the browser you are checking for.

  <script type="text/javascript">
      function getInternetExplorerVersion()
      // 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;
              var re  = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})");
              if (re.exec(ua) != null)
              rv = parseFloat( RegExp.$1 );
          }
          return rv;
      }
 
      function onLoad()
      {
          var version = GetInternetExplorerVersion()
          if (version <= 7 && version > -1)
          {
              // Code to run in Internet Explorer 7 or less.
          }
      }
  </script>

MSDN has a great article with more information if you’re interested: "Detecting Browsers More Effectively".

JavaScript Tutorials has a comprehensive article on how to use the navigator object and regular expressions to detect the various browsers and their exact versions.

Another way to detect browsers is by using Internet Explorer’s conditional comments. This syntax extends the standard HTML comments and is unique to Internet Explorer since version 5.

One way conditional comments can be used is with CSS style sheets. You can have certain IE-specific CSS rules—rules that you want other browsers to ignore. In the following sample, an "ie7.css" is loaded only if Internet Explorer 7 or less is detected.

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

You can read more detailed information on all the capabilities of conditional comments over at MSDN: "About Conditional Comments".

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

Feature Detection

The far better approach to handling differences in web browsers in your page is to use feature detection.

Before using a feature that you know has different implementations in the various browsers, you can run a small test where you look 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. If that instantiation returns something other than null, the executing browser knows the feature. If not, you can test to see if there’s a workaround or proprietary legacy implementation of the feature available.

Comparing Browser and Feature Detection

Sometimes diagrams help clear up the differences between the two in various situations.

image001.png

These are all the possible code paths that lead through our test site.

Well-known browser configurations

image002.png

When faced with well-known browser configurations, both 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

image003.png

Things get really interesting when faced with an unknown browser configuration.

Feature detection handles things well and finds out that this browser is capable of displaying Feature A but needs fallback code for Feature B. Browser detection picks a path because it only checks the browser’s name, or it simply chooses the default path when none of the queried browser/version combinations match.

Either way, in this example, the page will not render properly with browser detection because there is no code path that connects all the valid code segments, even though the page would have contained 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.

First, always test for standards, as a browser often supports the newer standard as well as the legacy workaround.

Second, only target related features in a single check, to minimize assumptions about a browser’s capabilities.

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 capsule feature detection into a set of functions that can then be used throughout the code. Here is a best practice on how to detect if the browser supports the HTML5 <canvas>, and if yes, makes sure that the canvas.getContext(‘2d’) method is working as well. It simply returns "true" or "false," making it easy to reuse.

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

When you’re using feature detection, always use it on newly created elements or objects. This helps you rule out the possibility that any other script on the page has modified it since it was created, which could lead to random or erratic results.

Feature detection also works directly for a select few HTML elements such as HTML5's <video>, <audio>, and <canvas> in 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 supporting the <video> element will show the video "video.mp4" and 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 doesn’t support videos natively.
  </video>

In this case, the 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 either or should the browser not support <video> at all, it will fall back to the text.

Instead of this text of course, it would make more sense to fall back to a plug-in based video player in case the browser does not support HTML5 <video> at all. The following sample uses a Silverlight video player for this:

  <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="http://url/player.xap">
          <param name="initParams" value="m=http://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, any 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 is easy to use and gets the job done for cases like this.

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

A big upside to feature detection is that it works with browsers you didn’t think of when creating your page. And since it doesn’t rely on any browser-based assumptions, it works in future versions of browsers as well.

Developing and Testing Feature Detection

When you’re ready to develop and test feature detection across many different browsers, you’ll want to check out the F12 Developer Tools in Internet Explorer 9. You can use it to debug script step by step, change the browser’s user agent string, and tell Internet Explorer to use the rendering engine of previous versions.

image004.png

Using breakpoints while debugging JavaScript in Internet Explorer 9.

image005.png

Running in "Document Mode: IE9 standards," the browser uses the modern "addEventListener" method.

image006.png

Running in "Document Mode: IE7 standards," the debugger falls back to the legacy "attachEvent" method.

image007.png

Change Internet Explorer’s user agent string on the fly using the developer tools and even add your own ones, including mobile browsers.

Managing Feature Detection in large projects

When creating a complex web project, creating and managing all the feature detection code yourself can be tedious.

Luckily, there are great JavaScript libraries available that help in this effort, namely Modernizr and jQuery.

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

Take a look at the following code to detect the browser’s capability to display web fonts:

Without Modernizr With 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;
  };
<script type="text/javascript" src"modernizr.custom.89997.js"></script>

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

"Help!" What to do when a browser doesn’t support the feature you need

If the browser tested doesn’t support a feature you need, feature detection won’t take the burden of coming up with a workaround from you. Sorry.

In the HTML5 video sample above, using Silverlight as a fallback was an obvious solution. But what about other HTML5 features like <canvas> or the new semantic tags in HTML5 including <nav>, <section> and <article>, <aside> or the new <header> and <footer>?

There are a growing number of ready-made "fallbacks" for most HTML5 features, called shims and polyfills. Shims and polyfills are CSS and JavaScript libraries (or sometimes Flash or Silverlight controls) that you can add to your project, which will then add in the missing, unsupported HTML5 features.

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 as users upgrade, your code doesn't have to change—users will seamlessly move to the better, native experience.

The difference between shims and polyfills is that shims only mimic the feature but has its own, proprietary API. Polyfills emulate both the feature and the exact API of the HTML5 feature it substitutes. 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 is easy to load other shims/polyfills if Modernizr detects that a feature is not supported.

"Won’t adding all these script libraries make my page huge and slow to load?"

Glad you asked! It’s true that adding many of these supporting libraries can add substantial overhead to your site. That’s why you shouldn’t go overboard—load them dynamically when required. With a shim or polyfill, the best practice is to only load them when you have detected that the browser really needs it because the native HTML5 feature is not supported.

Again, you’re in luck! There is a great library that supports exactly this scenario: yepnope.js

It’s 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 that 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 yepnope’s 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 will test the browser’s capability of using 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.

Summary: Dos and Don’ts

Let’s finish by summarizing the most important points:

  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.

Learn more about browser and feature detection at:

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here