Test-Driven Progressive Enhancement
Issue № 268

Test-Driven Progressive Enhancement

Progressive enhancement has become an established best-practice approach to standards-based development. By starting with clean, semantic HTML, and layering enhancements using JavaScript and CSS, we attempt to create a usable experience for everyone: less sophisticated devices and browsers get a simpler but completely functional experience, while more capable ones get the bells and whistles.

Article Continues Below

That’s the theory, at least. But in practice, enhancements are still delivered to most devices, including those that only partially understand them—specifically older browsers and under featured mobile devices. Users of these devices initially receive a perfectly functional page, but it’s progressively “enhanced” into a mess of scripts and styles gone awry, entirely defeating the purpose of the approach.

So how do we build enhanced experiences while making sure all users get a functional site? By testing a device’s capabilities up front, we can make informed decisions about the level of experience to deliver to that device.

Testing for capabilities#section2

Not long ago, we found that we could test portions of a device’s CSS support using simple JavaScript. It started with a simple box model test: we injected an element into the page, applied various styles to the element, and then used JavaScript to check whether the element was rendered properly.

function boxmodel(){ 
    var newDiv = document.createElement('div');
    document.body.appendChild(newDiv);
    newDiv.style.width = '20px';
    newDiv.style.padding = '10px';
    var divWidth = newDiv.offsetWidth;
    document.body.removeChild(newDiv);
    return divWidth == 40;
}

Using the function above, we can pose the question, if(boxmodel()) to find out if a browser properly supports the CSS box model. If a feature is properly supported, we can safely enhance our page using this feature.

With this idea in mind, we wrote additional functions to test support for other CSS properties, such as:

  • float
  • clear
  • position
  • overflow
  • line-height

But testing CSS support doesn’t cover everything. Fortunately, we can also use techniques such as object detection to test a browser’s JavaScript support. We wrote additional functions to test commonly used JavaScript Objects, such as:

  • document.createElement()
  • document.getElementById()
  • xmlHttpRequest()
  • window.onresize()
  • window.print()

We found that by running all of the tests above, we could get a good idea of whether or not a device would properly display the enhanced version of most of our clients’ web applications.

Following up on this premise, I, along with my colleagues at Filament Group, developed testUserDevice.js, which tests the capabilities of a device before providing enhancements. This script runs independently of any JavaScript libraries, weighs in at around 5kb, and executes in five or six milliseconds.

The tests run by testUserDevice.js can be modified, added, or deleted to meet the requirements of a given website. There are even options that allow you to run multiple test cases or a subset of tests to create multi-level user experiences.

Making use of the test results#section3

Integrating testUserDevice.js into a page is simple, just attach the JavaScript file to your page and call the following:

testUserDevice.init();

This will tell testUserDevice to begin running all available tests as soon as the body element is available in the DOM. If all tests return with a passing score, the script will proceed to make enhancements.

For the sake of this article, let’s take a look at a moderately complex form with some opportunities for enhancement. First, we start with the most basic HTML and a simple linear layout.

Demo 1

CSS enhancements#section4

Once a device has successfully passed a given set of tests, testUserDevice.js will enable page enhancements in a number of ways. By default, the following DOM modifications are made:

  • The class name enhanced is added to the body element.
  • Alternate stylesheet links with title attributes of (title=“enhanced”) are enabled.
  • Inversely, all stylesheet links with title attributes of (title=“not_enhanced”) are disabled.

These page modifications provide several hooks that will allow you to set up layers of progressive enhancement. For starters, the body class can be used along with CSS selectors to make small experience enhancements. For example, you might have three divs that are stacked vertically at page load:

body div.example {
     margin: 1em 0;
}

If the user’s browser or device passes the test, these divs could be repositioned into three floated columns:

body.enhanced div.example {
     float: left;
     width: 30%;
     margin: 0;
}
basic linear layout and enhanced floated layout
Divs repositioned into three floated columns.

This works well for pages that contain only a few enhancements, but it wouldn’t make sense to write an entire advanced stylesheet using these conditional selectors. For large enhancements, you can store advanced styles in alternate stylesheets. By giving these alternate stylesheets a title of enhanced, they will be enabled in the event of a passed test. For example, this:

<link rel="alternate stylesheet" type="text/css" href="enhanced.css" title="enhanced" />

becomes this:

<link rel="stylesheet" type="text/css" href="enhanced.css" title="enhanced" />

Of course, this is nothing more than a basic stylesheet switch, but the important point is that we’ve set up our enhancements to show only in devices that can handle them properly.

As mentioned earlier, testUserDevice.js will also disable any stylesheets specified by a title attribute of (title="not_enhanced"). This may not be necessary in most cases, but if you have basic stylesheets that will conflict with the enhanced ones, this is a nice way to keep them separated.

The following demonstration revisits our form example, now with a layout that is progressively enhanced with CSS in capable devices.

Demo 2

Although the demo above uses relatively simple CSS, it’s still important to test for device capabilities first since the layout may be unusable in a device that renders the CSS improperly.

JavaScript enhancements#section5

Many page enhancements use JavaScript. Fortunately, testUserDevice.js allows you to specify scripting that you would like to execute in your enhanced experience as well. This is done by simply passing in a function as an argument into our script, as follows.

testUserDevice.init( function(){ /* fancy stuff goes here */ } );

As you can see, we’ve passed an anonymous function as an argument to our init method. If our test passes, the scripting enclosed in the function argument will execute immediately. Keep in mind that this is likely to occur before your typical JavaScript library’s DOM ready event will fire, so DOM-related scripting should still be enclosed in an event that waits for elements to be available before executing (such as jQuery’s ready event or the body.onload event).

Our final demo page displays the same form as before, now progressively enhanced using JavaScript (in devices deemed capable) to its full feature set:

  • The birthday field has a date-picker component.
  • The preference selections are now sliders.
  • In the favorite pet fieldset, the “breed” fields are conditionally enabled by the chosen radio item.
  • The “breed” fields use auto-complete.
  • The form now uses Ajax for submission.

Demo 3

Once is all you need…#section6

Upon the completion of a test, testUserDevice.js drops a cookie recording the results of each test case and uses it on future page loads, which means less processing and better performance for users. But the opportunities to optimize don’t stop on the front end: with a little PHP handy work, you can actually look for these cookies on the back-end and serve pages knowing a device’s capabilities already! This means you could serve your page with an enhanced class already on your body element, enable your advanced stylesheets from the start, and execute advanced scripting immediately at page load.

<?php //check for cookie (enhanced=pass), and set the stylesheet rel depending on its value
$cssRel = ($_COOKIE['enhanced'] == "pass") ? "stylesheet" : "alternate stylesheet"; echo '<link rel="' . $cssRel . '" type="text/css" href="css/enhanced.css" title="enhanced" />'; ?>

Advanced usage#section7

testUserDevice.js includes options for handling custom test cases and adding your own tests.

Writing custom test cases#section8

Using testUserDevice’s default behavior of running a single test case is likely to be enough for most projects, but if you’d like to make multiple experience divisions, the option is available. For example, if we intended to make only two enhancements on our demo page:

  • Form submission via ajax
  • Select menus are converted into sliders

By default, we would test for all of the features required for these enhancements, and then enhance the entire page if the device passes all the tests. In contrast, we may want Ajax to work regardless of whether the selects are sliders or not (and vice versa). This is what I call multiple experience divisions. For this, we’ll need to write a custom test case, such as the following case which only tests a device’s Ajax support.

testUserDevice.init([
    {
        testName: 'ajaxCapable',
        pass: ['ajax'],
        scripting: function(){ useAjax(); }
    }
]);

Instead of passing a function into our init function as we did earlier, we’re now passing an array as an argument instead. This array contains one or more objects each containing three settings: testName (string, name of test), pass (array of test names corresponding to available tests), and scripting (function to execute upon passing test case). These test case objects can be used to make enhancements that only require a subset of features, letting you skip the all-or-nothing test.

Keep in mind that each test case’s testName property will be used to make DOM modifications in the same way our default test used the name “enhanced”. This means that upon passing test case above, our script will add a class of ajaxCapable to the body element, stylesheets will be enabled and disabled based on title attributes containing the word ajaxCapable, and a cookie will be saved as ajaxCapable=pass.

Adding your own tests#section9

All of the tests included in testUserDevice.js are stored in a tests object that can be easily modified to your needs. testUserDevice.js also comes with a handy helper function to let you add tests in a clean, separated manner. This helper function can be reached by calling testUserDevice.add(testName, testScript). For example, to add a test that makes sure a user’s browser supports a simple JavaScript confirmation, you could write:

testUserDevice.add( 'confirm', function(){ 
    if(window.confirm){return true;}
    else {return false;}
});

Or, more concisely…

testUserDevice.add( 'confirm', function(){ 
    return (window.confirm) ? true : false;
});

The add function’s first argument contains a string representing the name of this test. The second argument is the test itself, which must be a function that returns true or false (true for a passing grade; false for failure).

Developing for tomorrow, today#section10

As the device and browser landscape continues to grow and developers continue to find ways to implement more features and functionalities, our job of making universally usable websites will remain challenging. Integrating capabilities testing into our development process allows us to take full advantage of state-of-the-art features without ruining the experience for the users of less capable browsers and devices.

It’s important to remember that the approach described in this article does not endorse the use of any particular set of tests—just the idea that you should test to make sure capabilities exist before trying to use them. We welcome any ideas and suggestions on how our approach can be improved and extended.

24 Reader Comments

  1. I had looked at this technique upon reading about it on javascript guru John Ressig’s blog.
    An interesting and potentially very useful approach to browser sniffing.
    If you have to accomodate a wide variety of user-agents old and new, check it out.
    Thought-provoking at the very least.

  2. This is a very informative and a nice idea for sites that use a lot of JavaScript. I think the testing code would be overkill for smaller sites that aren’t using a great deal of JavaScript. Definitely something I’ll be looking into more though, and I’m a big fan of the progressive enhancements.

  3. Clearly using Javascript to test the browsers capability is clever and useful but what about users who do not have JS enabled? They end up seeing the basic version even though their browser may cope with the boxmodel or any other CSS perfectly well.

  4. @M Bruce: Good question and thanks for commenting. It’s true that enhancements that make use of the test script will not reach JS-disabled users, regardless of how advanced their browser may be. Assuming your site or application is functional in its “basic” mode, you might decide that this is acceptable, but keep in mind that it’s up to you to decide where the experience divisions are made and how extreme they may be. For example, your “basic” version might look quite similar to your enhanced version, aside from some more complicated enhancements that would require a blend of proper CSS and JS support (such as our sliders in demo 3).
    This technique provides a framework for making experience divisions in a clean and separated manner, but how and where you make them is entirely up to your discretion.

  5. This is great. I’ve always coded with standards and degraded properly, but *since recently getting a Sidekick and browsing the web more often while I’m out, graceful degradation has a renewed importance to me*. It sucks when I want to look up some basic information that happens to contained in Flash.

    But related to this article, it definitely sucks when I load a page that renders rather perfectly, but the status bar tells me that its forever loading a script, and eventually I’m prompted that the script should die.

    It’d be useful to make Javascript checks for functions and such before trying to throw them around.

  6. If you use JS to hide something that you would want to show to a client without JS, then you don’t ever run into the issue of it being inaccessible, and at the same time, you don’t have to worry about testing for effects. (ie, you have a cool animation on hover – well, put CSS :hover on like the animation doesn’t exist, and then use JS to override the whole operation.)

  7. The testUserDevice.js code has the following test for getElementById support:

    getById: function(){ return document.getElementById ? true : false; }

    Note that in IE Mobile 5, this will throw a JavaScript exception, because IE Mobile 5 doesn’t support this type of object checking. One example, but testing solely on object support forces you to make a lot of assumptions about the user agent which might not be true.

    Another example: Google Chrome, like all WebKit browsers supports border-raduis, but “you wouldn’t want to use it”:http://www.flickr.com/photos/kurafire/2822606444/ .

    I think Yahoo’s “Graded Browser Support”:http://developer.yahoo.com/yui/articles/gbs/ is a far better idea. It’s just not efficient to do this kind of testing, as your results will still be sub-par.

  8. Lately, I’ve been making extensive use of psuedo classes like :hover and :focus knowing very well that they won’t be supported in all browsers.

    The thing is, knowing that I’m going to do this influences my design, so I make sure that design is clear enough where these sort of CSS filigree don’t break the user experience in the slightest.

    My €.02 also thinks that sniffing for a mobile device on the server side can provide a richer experience with less effort. Mbta.com spits out a rich site when viewed on the desktop, but simple text when viewed on a mobile device. Then they’re only upkeeping a few extra templates specifically designed for mobile, whereas this solution involves juggling javascript, stylesheets, etc.

  9. Something about sniffing for device capabilities seems so ancient from a design standpoint.
    We used to do this for browsers and deemed it as irrelevant and futile.
    Why would we do this for devices when we have alternatives via good, standards based code and CSS media styles?

    Sounds like another solution to a design problem that really doesn’t need to be “fixed”.

  10. @Ryan Cannon: Thanks for commenting. You brought up some good points and I’m glad you mentioned Yahoo’s Graded Support. I think an ideal approach could be a combination of capabilities testing AND following a browser support matrix such as Yahoo’s. Correct me if I’m wrong, but to my understanding, Yahoo’s Graded support acts as more of a list of browsers in which a site has been fully QA’d to support, but anything beyond those may or may not receive a usable website, and they fall into groups C or X. It’s more of a support disclaimer than an attempt to provide a usable experience for all. In other words, without some sort of capabilities testing in place, browsers in groups C or X might actually get a completely unusable website instead of a usable one with less bells and whistles. But even if you are conditionalizing your features with capabilities testing of some sort, you’ll surely still have a set of target browsers to support, and that’s where something like Yahoo could come in. This technique just aims to give users with a C or X grade browser a working experience of some sort.

    As for the IE mobile 5 object detection error, that’s good to know, and something I wasn’t aware of. Of course, you can decide which tests to run based on your needs. Perhaps there’s a way to write those particular tests to make sure Mobile IE 5 is included when it should be. We’ll look into this further.

  11. @Steve: The problem lies in how that “good, standards based code” will function on Grampa’s computer * . You’re in good company saying that sniffing for user agents is generally considered bad form. This approach is different from sniffing in that we’re never asking what browser you’re using, we just care if it works well enough to handle a certain feature.

    @Erika: We tested the demos in iPhone and deemed them usable. Try tapping on each slider scale to move the handles. In a real implementation though, you might decide these would be better left as select menus for iPod users. After all, you aren’t really “sliding” them anyway, and they might even be easier to use that way on iPhone. This would just mean conditionalizing the slider enhancements with a stricter test.

    *sorry, Grampa 🙂

  12. …sorry for the bold text. Apparently asterisks don’t render out in this system.

    Please place mental asterisks after “Grampa’s computer” and before “sorry, Grampa”… sigh 🙁

  13. I’ve looked into progressively enhancing forms a little bit of late, the only concern I have is how best to provide an opt-out for those who may be reliant upon screen readers etc where AJAX seems to be a hit and miss affair. What is a best practice here? Provide a link that sets a cookie, pages then check if cookie exists and enhance based upon preference?

  14. Progressive enhancement does sound like a good idea to me. Reading through all the comments it seems that there is a mixed response. My thoughts are that anything to ensure the end user/visitor gets a good experience that reflects well on the organisation the site represents is a good thing.

    The use of CSS media types is a good idea – we are already doing this for printing (I hope) so why not use it for mobile devices. However, there are limitations with this in that it is an all or nothing approach to the problem. Is there some way of combining the CSS media type solution with the use of this JS? If we had the stylesheets loaded correctly at the start then the technique in this article could be applied over the top – the default, or fall back, stylesheet would then be correct for the media type.

    I hope that makes sense – I’ve not been up long and my coffee has run out.

  15. Shouldn’t the Progressive enhancement run something like:
    HTML -> CSS -> JavaScript -> (Flash / Ajax / Silverlight / etc)
    Doesn’t this method skip to step three to test step two?

    I’ve always found that CSS media type solves the mobile devices issue and IE’s non-standard conditional comments fix their own shocking CSS support. The ONLY issue I have is Safari which has the odd bug but is impossible to target without hacks. (As far as I know?)

    As long as you test for each object / method you use in you JavaScript before implementing the code, there shouldn’t be any problems there either.

    Everything else is bells and whistles and shouldn’t hamper the user experience.

  16. bq. To my understanding, Yahoo’s Graded support acts as more of a list of browsers in which a site has been fully QA’d to support, but anything beyond those may or may not receive a usable website, and they fall into groups C or X. It’s more of a support disclaimer than an attempt to provide a usable experience for all. In other words, without some sort of capabilities testing in place, browsers in groups C or X might actually get a completely unusable website instead of a usable one with less bells and whistles.

    This isn’t quite right. YGBS limits browsers to three scenarios:

    # You’re ok, we’ll give you everything and fix the bugs we find.
    # You’re terrible. You get no CSS/JS, just a sparse but usable Web site, and we’ll make sure you can use it.
    # We don’t know you, but there’s no way we can cover all the bases. If you act like the standards and precedences say you should, then everything should be ok.

    So there’s much *less* guesswork than trying to plan for many different types of failure secenarios.

    bq. Perhaps there’s a way to write those particular tests to make sure Mobile IE 5 is included when it should be. We’ll look into this further.

    I didn’t want to pimp my own blog here, but I did some of that work for you in my article “The Mobile Dollar”:http://ryancannon.com/2008/07/10/the-mobile-dollar-dipping-a-toe-in-device-javascript .

  17. @Scott Jehl, Thanks for the navigation hints. 🙂

    First, a disclaimer: iPhone is the first mobile browser I’ve used, and I’ve only had it a week. Though IMO that makes me a perfect tester. 😉

    Second, yes! the “sliders” work on iPhone if you click the location you want them to “slide” to. Indeed, they do not actually slide, but on the desktop browser, it all seems relatively intuitive (to me). On the iPhone, it is not intuitive (to me). I guess that certain Javascript events are going to be different on the mouse-less device. In general, I feel more basic, intuitive, or at least, “de facto standard” navigation elements make sense to a mobile device. And should be delivered with a media-type stylesheet…?

    Also, this article, and the iPhone, is making me think that the concept of “progressive enhancement” is now something of a misnomer, or at least, only part of the issue.

    iPhone Safari (for example) is not a *lesser*, but a *different* browser.

    While it is !important that a browser understands the DOM (and iPhone Safari offers some really neat functionality around block-level elements) — once that part is out of the way, there are a whole lot of other important things. What are the special needs of the user, and how is the device/software delivering content? And once you know you *can* do something … *should* you?

    With the renewed focus on Javascript… I think we should also keep our focus on usability as a very important part of the design/development process, especially when dealing with highly interactive elements, like forms.

    Just some thoughts, tangentally related…

  18. Isn’t it enough if we know which browsers we are targetting rather than the specific features of the browser that in loading at the user’s end?

    What is the advantage of this over simply getting the browser useragent? Would be great if we had a practical example!

  19. @Erika Meyer: I agree with you. Perhaps the demos should have resulted in a different experience for iPhone users (via media type, sure). I intended the demo page to simply illustrate the idea of test-driven enhancement in a way that seemed somewhat easy to follow. How a site will actually implement their enhancements across devices is left to the developer and the test cases they choose to include. Thanks for your insight, though. As I’d said earlier, one might decide that iPhone users would be better off using select menus, particularly due to the nice native wheel interface that iPhone provides!

    @Divya Manian: It’s a good question. User agent sniffing is a different approach and comes with its disadvantages. For one, the approach is limited to browsers you actively sniff for, and doesn’t attempt to cater to the experience of users on devices that aren’t in your set of agent strings. Feature (or bug) detection is a different approach in that it acts upon capabilities of a browser, regardless of what that browser it is. We’re seeing javascript libraries like jQuery moving towards feature/bug detection rather than user agent sniffing for this very reason. Feature detection is also forward-looking in that it adapts to bug fixes in browsers as they happen. For instance, if you were to fork some code based on a known implementation difference in IE7 using a user agent sniff, and IE7 releases an update with a fix to that bug, the browser will no longer will need that fork. A user agent check would continue to direct IE7 to that forked code, as opposed to feature/bug detection, which would direct it to the code that aligns with its updated implementation.

  20. This is a pretty cool idea brought to fruition! It really brings Javascript and browser detection to a whole new level. I’m surprised this CSS detection technique hasn’t been mentioned as often as it should be.

    Thanks for the update! =)

  21. There is one thing that bothers me. The markup has to be written for the most basic browser that we want to support. Since the markup is static how can our application get advantage of new semantic tags that are going to be introduced in the future releases of html?

  22. The techniques first described in this A List Apart article have since been refined and built into a small JavaScript framework called EnhanceJS. EnhanceJS allows you to easily include scripts and stylesheets in browsers deemed capable of rendering them properly, and comes with an extensible API for running custom capabilities tests and taking advantage of several advanced features.

    Info on EnhanceJS:
    * “EnhanceJS project site”:http://enhancejs.googlecode.com]
    * “EnhanceJS Intro article”:http://filamentgroup.com/lab/introducing_enhancejs_smarter_safer_apply_progressive_enhancement/

Got something to say?

We have turned off comments, but you can see what folks had to say before we did so.

More from ALA

I am a creative.

A List Apart founder and web design OG Zeldman ponders the moments of inspiration, the hours of plodding, and the ultimate mystery at the heart of a creative career.
Career