Same Markup: Writing Cross-Browser Code

I recently presented a session at MIX10 covering the topic of cross-browser best practices. The key focus of the session was not on a particular feature, but on how web developers can reliably write code that adapts to cross-browser differences.

IE9 reduces these differences and enables developers to use the same markup across browsers. Enabling the same markup means supporting the right features to make the same HTML, JavaScript, and CSS "just work". Yet enabling the same markup on the web is an n-way street. Each browser must provide the right features, but developers also need to properly detect and use those features when they are available.

With respect to properly detecting features I'll share examples of issues and best practices I've seen on the web. Most importantly I'll offer patterns that can help you rewrite similar code to take advantage of the same markup. For now I'll focus on more basic examples to illustrate the core concepts, but future posts will cover more complex examples in greater detail. Let me start by sharing a set of guidelines:

Same Markup: Core Guidelines

  • DO
    • Feature Detection
      Test whether a browser supports a feature before using it.
    • Behavior Detection
      Test for known issues before applying a workaround.
  • DON'T
    • Detect Specific Browsers
      Also known as browser detection. Don't use the identity of a browser (e.g. navigator.userAgent) to alter page behavior.
    • Assume Unrelated Features
      Don't perform feature detection for one feature, and then proceed to use a different feature.

These guidelines are important because most web pages today are a hybrid of code meant for multiple different browsers. Mixed in with this code are the tests for choosing what runs where. The conditions used in such tests determine how a page adapts to run on different browsers. From script at least, these tests generally take the following form:

 if( condition ) {
    // Primary Code
} else {
    // Alternate Code
}

The conditions used in such tests are often not based on whether a given feature is available, but rather on which browser is in use. Therein lies the problem: altering code based on a specific browser limits the adaptability of web pages. The end result can be as serious as the page breaking when a new browser is released. Other times a legacy workaround continues to be used, even when that workaround is no longer needed.

DON'T: Detect Specific Browsers - Event Registration Example

You can easily test the effects yourself. The code below switches between event models based on the detected browser (a bad practice). Running this in IE9 illustrates that addEventListener doesn't get used, even though it's supported.

 // [TR] Different listeners added for illustration
function f1() { document.write("addEventListener was used"); }
function f2() { document.write("attachEvent was used"); }

// DON'T USE THIS: Detecting specific browsers
if(navigator.userAgent.indexOf("MSIE") == -1) {
    window.addEventListener("load", f1, false);
} else {
    window.attachEvent("onload", f2);
}

Try It!

IE9 Output: attachEvent was used

DO: Feature Detection - Event Registration Example

The following code shows how to switch between event models correctly using feature detection. Instead of checking for IE, it checks for the availability of addEventListener itself. This code will continue to fallback correctly in legacy browsers without addEventListener, but more importantly will now ALWAYS use addEventListener when it is available. In short, this approach enables running the same markup in IE9 and other browsers.

 // [TR] Different listeners added for illustration
function f1() { document.write("addEventListener was used"); }
function f2() { document.write("attachEvent was used"); }

// Use this: Feature Detection
if(window.addEventListener) {
    window.addEventListener("load", f1, false);
} else if(window.attachEvent) {
    window.attachEvent("onload", f2);
}

Try It!

IE9 Output: addEventListener was used

The challenge of getting the right code to run in the right browser is why feature detection has been seeing increased adoption in pages and frameworks. Feature detection enables cross-browser code to "just work" without requiring you to know the capabilities of each and every browser ahead of time. One framework which relies almost entirely on feature detection is jQuery. In fact the jQuery.support documentation details how you can use jQuery's feature detection in your own site.

DO: Behavior Detection - getElementById Example from jQuery

In addition to straight feature detection, jQuery also makes extensive use of behavior detection. It does this by running a tests for known issues to determine if certain workarounds are needed. Below is a slightly modified snippet from the jQuery source code that tests whether getElementById includes elements with "name" attributes. This is a legacy IE bug that was fixed in IE8.

 // We're going to inject a fake input element with a specified name
var form = document.createElement("div"),
id = "script" + (new Date).getTime();
form.innerHTML = "<a name='" + id + "'/>";

// Inject it into the root element, check its status, and remove it quickly
var root = document.documentElement;
root.insertBefore( form, root.firstChild );

// The workaround has to do additional checks after a getElementById
// Which slows things down for other browsers (hence the branching)
if ( document.getElementById( id ) ) { 
    // ... Workaround code ...
      
    // [TR] Added for illustration
    document.write("getElementById workaround was used");
}
// [TR] Added for illustration
else document.write("No workaround was used");

root.removeChild( form );

Try It!

IE9 Output: No workaround was used

DON'T: Assume Unrelated Features - Real-World Example

The final piece I want to touch on is assuming unrelated features. This is something we saw a number of examples of as we did compatibility testing for IE8. Sites suffering from this problem would perform feature detection for one feature, then continue to use a variety of other features without testing whether they were supported. The following modified example illustrates a case where a site was using the postMessage and addEventListener APIs, but was only testing for the former. Support for postMessage was added in IE8, but addEventListener was not added until IE9. If you run this example unmodified in IE8, it results in a script error, preventing any following script from executing.

 // [TR] Try-catch block added to trap the script error in IE8
try { 
    function fn() {}

    if(window.postMessage) {
        window.addEventListener("message", fn, false);
    
        // [TR] Added for illustration
        document.write("Message listener registered successfully");
    } else {
        // ... workaround for when postMessage is unavailable ...
      
        // [TR] Added for illustration
        document.write("postMessage is not supported, using workaround");
    }
    
} catch(e) {
    document.write("Message listener registration FAILED");
}

Try It!

IE7 Output: postMessage is not supported, using workaround
IE8 Output: Message listener registration FAILED
IE9 Output: Message listener registered successfully

DO: Feature Detection - Test for Unrelated Features Independently

A corrected version of the above example is provided below. It checks for BOTH postMessage and addEventListener before using them.

 function fn() {}

if(window.postMessage) {
    if(window.addEventListener) {
        window.addEventListener("message", fn, false);
    } else if(window.attachEvent) {
        window.attachEvent("onmessage", fn);
    }
  
    // [TR] Added for illustration
    document.write("Message listener registered successfully");
} else {
    // ... workaround for when postMessage is unavailable ...
    // [TR] Added for illustration
    document.write("postMessage is not supported, using workaround");
}

Try It!

IE7 Output: postMessage is not supported, using workaround
IE8 Output: Message listener registered successfully
IE9 Output: Message listener registered successfully

Moving Forward

I've outlined the benefits of using feature and behavior detection over detecting specific browsers, but my intent is for this to be the start of a conversation, not the end of it. Watch for future posts covering more detailed examples of issues we've seen on the web and how they can be coded to work correctly across all browsers. We'll also be examining our own sites and encouraging them to update to follow these practices. Please feel free to ask questions and share insights, especially around specific issues you've encountered while developing your own pages.

Tony Ross
Program Manager