Delivering the right experience to the right device

Posted by Maggie, Scott, and Todd on 03/07/2008

Topics:

NOTE: We made major improvements to this script in February 2010 and renamed it EnhanceJS. We recommend that you read the updated post: Introducing EnhanceJS: A smarter, safer way to apply progressive enhancement. The original logic and functionality is outlined below.

09/23/08 Update: In issue 268 of A List Apart, our own Scott Jehl discusses our process of Test-Driven Progressive Enhancement. The article introduces a fresh approach to our earlier research on the topic, and includes a full rewrite of the Javascript which is faster and more flexible for development. We will post details on the script API in the lab shortly. Article link: Test-Driven Progressive Enhancement.

Inspired by Jeremy Keith's concept of Hijax, we've adopted the practice of coding with progressive enhancement in mind to ensure optimal accessibility, compatibility with multiple browsers and platforms, and ease of maintenance for each site or web application. But how do you do that when so many browsers only partially support javascript and CSS techniques? We met the challenge by developing a script to test the browser's ability to handle modern coding techniques, and ensure a usable experience for all.

Why is progressive enhancement important?

We presented this technique at the Markup & Style Society on March 13, 2008. Download Slides (1mb PDF)

In a word: accessibility. Most of the web sites and applications we design and code are intended for a wide audience, be it a targeted segment of the general public, the client's own organization, or both, and our intention is to provide a usable experience to everyone. In most cases that means developing front-end code that supports the required functionality and design elements on a variety of browsers and platforms, some of which are standards-compliant and render complex Javascript animations and visuals flawlessly, and some of which render those same modern script and style techniques poorly (and that's putting it kindly). The ongoing challenge is to develop code that provides the richest experience to the broadest audience, and at the very least a clean and usable experience to those using non-compliant browsers.

Coding for progressive enhancement allows us to meet that challenge with a single code base. (In some exceptional cases it may make sense to fork the code to meet the functional requirements of different devices, but most often we find that creating and maintaining multiple code bases is unnecessary when the it's written with progressive enhancement in mind.)

We start with plain, structured, semantic HTML and layer on functionality to provide an enhanced experience to capable browsers. The end result of this approach is:

  • Modern, standard-compliant browsers render a rich, interactive experience including accurate rendering of the visual designs with multi-column layouts, background colors and images, and Javascript-based interaction and animation.
  • Depreciated, mobile or text-only browsers render clean, fully usable and well-formatted pages with minimal style and without interaction effects. As an added benefit, this approach also makes it possible to provide a clean, well-structured experience for users accessing the sites from mobile devices, screen readers and other accessibility support aids.

How do you know if a browser supports advanced CSS and Javascript?

There are a few well-known ways to prevent non-compliant browsers from rendering particular styles, like using tricky reference syntax, or doing object detection to prevent a browser from executing a specific function. But we found ourselves running into cases of partial support, where a few browsers would support some but not all of the latest CSS or DOM specification correctly.

Rather than complicate our markup and scripts with numerous exceptions or resort to the less reliable method of browser version sniffing, we decided to take matters into our own hands and created a comprehensive test to determine if a browser is capable of doing what we want it to do: floated column layouts, interactive widgets, animation effects, and so on. If the browser passes, we apply all of our styles and complex interactions; if the browser fails, it skips the fancy CSS and Javascript and renders a basic, clean — and fully usable — version of the site.

Our solution: use javascript to detect CSS

We can figure out which browsers support the richest experience by a process of elimination. To begin, if javascript is disabled, the test won't run and the page loads with basic HTML and styles. But if javascript is enabled, we can run the test and apply styles using CSS, and then test their display accuracy using common javascript equivalents. For example, we can set the width on an element with css and then check its width using offsetWidth. Or we could float two elements and then evaluate their offsetTop values for equality.

What we test for

Immediately after the body element is ready, testUserDevice.js evaluates a device's ability to handle the following css properties properly:

  • Box model: make sure the width and padding of a div add up properly using offsetWidth
  • Positioning: position a div and check its positioning using offsetTop and offsetLeft
  • Float: float 2 divs next to each other and evaluate their offsetTop values for equality
  • Clear: test to make sure a list item will clear beneath a preceding floated list item
  • Overflow: wrap a tall div with a shorter div with overflow set to 'auto', and test its offsetHeight
  • Line-height (including unitless): test for proper handling of line-height using offsetHeight, primarily to rule out Firefox 1.0

It also tests the following javascript capabilities:

  • Basic DOM traversal
  • Window Resize Detection
  • Printing
  • A little additional object detection to rule out certain browsers that have known CSS bugs that cannot be easily tested for, like faulty background-image rendering in Netscape 7.0

If the browser fails any of the above tests, the script will return false and the browser will render the clean, well-structured markup on the page with few styles and no interaction effects — users will still have access to all of the content, but will just experience a plainer version of it.

Generally, these tests are set up to target known issues of devices we do not intend to support, but they are performed in a way that is unbiased to the device being tested and will work with future devices as well.

What happens when a device passes?

Upon successful completion of the test, advanced styles and interaction effects are applied to the basic markup through the DOM:

  • Any script passed in as an argument will be forwarded along to execute on DOM ready. (The test script contains a library-independent DOM-ready function but it will use jQuery's instead if it is available.)
  • A class of "enhanced" is assigned to the body element to be used for optional CSS scoping (such as: body.enhanced {background: red;})
  • Any links to alternate stylesheets that have a class of "enhanced" will be enabled. In order for this to work, each alternate stylesheet must have a title attribute.
  • Any links to stylesheets that have a class of "basicNoCascade" will be disabled. This is optional depending on whether your enhanced stylesheets cascade or conflict with your basic sheets.
  • A cookie is set (enhanced = true) to notify the script on future page loads that the browser has already passed and can receive an enhanced experience.

Users with successful browsers will experience the design as intended, with the complete and intact visual design and complex interactivity.

Usage

To apply testUserDevice.js, simply grab the file linked below and link to it in the head of your html file. Enclose any DOM-related script executions within the following function:

 enhancedDomReady(function(){
  //put scripts here to be executed after the 
  //dom is ready and the browser has passed the test
 });

Download Script: testUserDevice.js

Results Matrix

By running this test, we're able to ensure that all browsers render a usable experience to our entire audience — the only difference is that browsers that fail render well-structured HTML with minimal styling and no interaction effects, while browsers that pass render the enhanced experience with rich visuals and fully interactive elements. The following tables represent the division of browsers:

Platform Browser Version Pass/Fail
Linux Fedora Core 4 Firefox 1.5 Pass
Linux Fedora Core 4 Firefox 2.0 Pass
Linux Fedora Core 4 Konqueror 3.4 Fail
Linux Fedora Core 4 Mozilla 1.7 Pass
Mac OSX 10.2 Explorer 5.1 Fail
Mac OSX 10.3 Camino 1.0 Pass
Mac OSX 10.3 Explorer 5.2 Fail
Mac OSX 10.3 Firefox 1.5 Pass
Mac OSX 10.3 Firefox 2.0 Pass
Mac OSX 10.3 Mozilla 1.6 Fail
Mac OSX 10.3 Mozilla 1.7 Fail
Mac OSX 10.3 Netscape 7.2 Fail
Mac OSX 10.3 Opera 8.5 Pass
Mac OSX 10.3 Opera 9.0 Pass
Mac OSX 10.3 Safari 1.2 Pass
Mac OSX 10.3 Safari 1.3 Pass
Mac OSX 10.4 Camino 1.0 Pass
Mac OSX 10.2 Explorer 5.1 Fail
Mac OSX 10.2 Explorer 5.2 Fail
Mac OSX 10.4 Firefox 1.5 Pass
Mac OSX 10.4 Firefox 2.0 Pass
Mac OSX 10.4 Mozilla 1.6 Fail
Mac OSX 10.4 Mozilla 1.7 Fail
Mac OSX 10.4 Netscape 7.2 Fail
Mac OSX 10.4 Opera 8.5 Pass
Mac OSX 10.4 Opera 9.0 Pass
Mac OSX 10.4 Safari 2.0 Pass
Windows 2000 Professional AOL 9.0 Pass
Windows 2000 Professional Explorer 5.0 Fail
Windows 2000 Professional Explorer 5.5 Fail
Windows 2000 Professional Explorer 6.0 Pass
Windows 2000 Professional Firefox 1.5 Pass
Windows 2000 Professional Firefox 2.0 Pass
Windows 2000 Professional Mozilla 1.6 Fail
Windows 2000 Professional Mozilla 1.7 Pass
Windows 2000 Professional Netscape 4.7 Fail
Windows 2000 Professional Netscape 6.2 Fail
Windows 2000 Professional Netscape 7.2 Fail
Windows 2000 Professional Opera 8.5 Pass
Windows 2000 Professional Opera 9.0 Pass
Windows 98 Explorer 4.0 Fail
Windows Vista Explorer 7.0 Pass
Windows Vista Opera 9.0 Pass
Windows XP Explorer 3.0 Fail
Windows XP Explorer 4.0 Fail
Windows XP Explorer 5.0 Fail
Windows XP Explorer 5.5 Fail
Windows XP Explorer 6.0 Pass
Windows XP Explorer 7.0 Pass
Windows XP Netscape 4.0 Fail
Windows XP Netscape 6.2 Fail
Windows XP Netscape 7.0 Fail
Windows XP Netscape 7.1 Fail
Windows XP Netscape 7.2 Fail
Windows XP Netscape 8.0 Pass
Windows XP Opera 7.5 Fail
Windows XP Opera 8.0 Pass
Windows XP Opera 8.5 Pass
Windows XP Opera 9.0 Pass
Windows XP Firefox 1.0 Pass
Windows XP Firefox 1.5 Pass
Windows XP Firefox 2.0 Pass
Windows XP Mozilla 1.6 Fail
Windows XP Mozilla 1.7 Pass
Book cover: Designing with Progressive Enhancement

Enjoy our blog? You'll love our book.

For info and ordering: Visit the book site

Comments

Great work guys! I’ve put up a quick blog post on this technique:
http://ejohn.org/blog/progressive-css-enhancement/

Comment by John Resig on 03/15  at  10:41 AM

Very nice work. I do like the test script. This is a a gap however, of people who are running a browser capable of A grade CSS, but have Javascript switched off (I’m not aware of any firm figures for how many have Javascript off). They then miss out on all the ‘enhanced’ styles.

Comment by Allan Jardine on 03/15  at  12:01 PM

You guys joked that the title could use some work. The first thing my brain jumped to was calling it “bouncer.js”. It alludes to what the script is doing and is succinct.

p.s. Doesn’t “browserBouncer” sound like an 80’s video game?

Comment by Jake Harvey on 03/15  at  06:04 PM

@Allan: I think that’s an acceptable loss. In my case, at least, I think I would be happy to consider a browser that has JavaScript disabled to be a “fringe/unsupported” browser. Because, really, unless you’re actively targeting that demographic it probably should receive that degraded experience. That’s not to say that the degraded experience should be poor, or unusable, just that it won’t, explicitly, be as easy to use as the full experience (maybe they have to use a drop-down instead of a slider - or a list instead of a menu).

Comment by John Resig on 03/15  at  06:22 PM

@Jake: Bouncer.js! That’s awesome:) Of course we’ll need to meet internally on the matter ;) hah

@Allan: I agree with John. If you are wondering about the slider/menu reference he mentioned, you’ll need to view the slides from my presentation on this technique here:
http://www.filamentgroup.com/lab/markup_and_style_recap/

Comment by Scott (Filament) on 03/15  at  06:34 PM

@ John, Scott: I fully agree with you, that for certain projects, and dependent on developer time available, grouping A grade CSS and Javascript support together for browsers is a reasonable thing to do.

However, by the principle of progressive enhancement, if a browser is capable of displaying the enhanced styles (A grade CSS) then it should really be given the chance to do so, regardless of Javascript settings.

Comment by Allan Jardine on 03/16  at  12:38 PM

@Allan: It’s an interesting concern, and one that I wish we could safely support. It seems to me though that the group with javascript disabled would be more of the edge case rather than the one we’d develop to in the wide scope of web users. Again, if a significant audience of an application is known to use script-less devices, the app might be developed accordingly. But in giving a non-script A-grade browser all the goodies, you’re giving all of the B-grade browsers an “A” as well. It’s safe to assume that some users will not be able to use your application because of that decision.

If we’re using the grading system as an example; what makes a browser Grade-A? An excerpt from yahoo:

“A-grade support is the highest support level. By taking full advantage of the powerful capabilities of modern web standards, the A-grade experience provides advanced functionality and visual fidelity.”

From that, I would reason that A-grade support is given to a browser capable of both A-grade presentation and functionality (CSS and JS). With javascript disabled, would that browser be A-grade anymore?

Comment by Scott (Filament) on 03/16  at  03:38 PM

I hope that using JavaScript to test for CSS support will be a growing trend and help eliminate CSS hacks. I recently wrote an article about combining CSS support testing with as an integral part of building a JavaScript widget that depends on particular CSS support for its functionality (http://peter.michaux.ca/article/7217).

Comment by Peter Michaux on 03/16  at  06:19 PM

@Scott: I completely agree that the application should develop as the user needs require, and as developer time permits. Developing for fringe cases can sometimes be a real pain (the 80/20 rule will tend to come into play...). I also agree that without Javascript, no browser can be A grade in overall terms.

I’ve just written up an article about this subject (http://www.sprymedia.co.uk/article/Graded+Technology+Support), with my view of it, which you might be interested in. It basically allows for this situation.

One example of being able to use CSS grade A when you don’t have Javascript available that I like is formatting UL tabs. Typically we format a tab list using hidden div’s and a UL tag, with some (potentially complex) CSS formatting, and then loading new content into the tabs with Javascript. However, without Javascript, the tabs should still load the new content (it will just be at the cost of a page refresh), so without Javascript, surely the visual layout should be exactly the same as with?

Comment by Allan Jardine on 03/16  at  09:24 PM

Doh - sorry about that - I should have guessed HTML tags wouldn’t be formatted…

Comment by Allan Jardine on 03/16  at  09:26 PM

@Allan: Thanks for the feedback and followup.
In any case, the developer will decide where the division of experiences takes place. By giving an enhanced presentation to an A-Grade device that has JS turned off, you will need to be comfortable with that same presentation rendering properly and being usable in the oldest/worst of devices. We tend to aim for a safer divide by giving that non-js device a simple (yet perfectly usable) experience because there’s no way to be sure what it can handle. If you are familiar with how CSS is interpreted on an old Treo or something similar, you probably know where we’re coming from.

So in the case of tabs, our simple experience just might be a more linearized view of the content where the tabs degrade to a list of links to their respective pages, followed by the content for the currently selected “tab”. Perhaps you’d want to add a bit of style to the simple experience to make it look like a tab strip, but the situation mentioned above would certainly apply.

In using the test this way, we are supporting the fringe cases as well as the status quo with a usable application. In the end, the primary goal is for the app to be accessible and usable to the broadest audience possible.

Comment by Scott (Filament) on 03/16  at  11:57 PM

@Allan, I think you’re right that it shouldn’t be all or nothing, but what this script allows you to do (if I understand it correctly) is serve up different CSS (and JavaScript) to the user-agent depending on capabilities. If the browser fails, it would be reasonable to display elements that might have “display: none;” set, but otherwise serve up the same, nicely designed layout, for example.

Comment by Andrew Hedges on 03/17  at  02:53 AM

@Andrew and Allan: Thanks guys, you both make good points. The way the enhancements are divided is ultimately left to the developer to decide. The test simply allows for a clean division to take place.

Comment by Scott (Filament) on 03/17  at  05:19 AM

This looks like a great script. Any info on extra delays to the user with this?

Also, I’m surprised Konqueror 3.4 fails as it has almost complete CSS 2.1 support and some CSS 3. I think devices like the PSP have fairly good CSS too, but no JS support.

Any chance of having 3 groups of browsers:
A grade - with everything, eg FF2+, IE7+.
B grade - OK CSS and JS, but not suitable for things like ajax, eg newer mobile browsers and older desktop browsers.
Everything else - Very old browsers and those with no / little CSS and JS.

Comment by Dave on 03/17  at  11:46 AM

@Dave: We actually use the test on this site so you can see how the script impacts rendering performance. At most, you might have seen a very tiny flicker on the first page load while the test was being run and the enhanced experience was being “upgraded”. After that, the cookie (if available) will keep the test from needing to re-run so there shouldn’t be any appreciable impact at all.

We think that making this script configurable so developers can decide how many grades they want to parse devices into and which specific tests to run is a great idea. We just happen to be using two levels of support for simplicity right now, but many situations could benefit from more grades of support. Hopefully, someone from the community will take our script and add this level of modularity. We’ll also continue to refine this script as well, so there will probably be a version 2.0 eventually…

Comment by Todd (Filament) on 03/17  at  05:41 PM

@ Scott and Todd: I think you are absolutely right, it is down to the developer to decide how much support they want to offer. There is simply no point if there is no market for it…

@ Scott: The idea behind graded technology support is that you need only be confident that a particular technology is supported to the required degree. The interaction (while it should be thought about) should be minimal (in the standard display / behaviour / content break down that we all know). I’ve quickly bashed together an example of the tabs I talked about:

(http://sprymedia.co.uk/media/reflections/gts/tabs.php)

Try it both with and without Javascript, and with and without CSS. Graded technology support (and progressive enhancement) allow us to deal with this.

I think between us the two methodologies cover just about all situations!

Also - I’ve not tried writing styles for the Treo, but I have done for Pocket IE. I now have considerably less hair than when I started…

Comment by Allan Jardine on 03/18  at  12:20 AM

@Allan: Back to the earlier comments: we just posted an article about the slider John referenced above:
http://www.filamentgroup.com/lab/developing_an_accessible_slider/

Comment by Scott (Filament) on 03/21  at  09:53 PM

I’ve been playing with this for a couple days and generally like the idea. Like Allan, I have a concern about CSS advanced browsers that don’t have JavaScript enabled for whatever reason. But it’s not the reason I’m posting.

One thing I’ve noticed on your site as well as the pages on which I implemented the test is that the class name of ‘enhanced’ is added twice to the <body>. It looks like from the functions that you’re calling ‘enhanceDocument()’ twice but not checking if it’s already been added to the body. Is there a reason it’s added twice?

Also, I had some initial confusion about your use of the term ‘alternate stylesheet’. I was thinking you actually meant a <link> with rel="alternate stylesheet”. When I did that, the script never activated the enhanced stylesheet. It only did so once I changed the ‘rel’ to “stylesheet”. Putting that out there since others might have did the same.

Thanks for putting this out there. Cool stuff.

Comment by Travis Forden on 03/27  at  05:22 PM

@Travis:

Good catch, we’ve updated the script to apply “enhanced” only once.

Re: alternate stylesheets, your first take was right—any alternate stylesheets (rel="alternate stylesheet") with the class “enhanced” will be enabled upon passing the test. Keep in mind that an alternate stylesheet link must also have a title attribute in order to work (this tidbit is buried in the HTML spec: http://www.w3.org/TR/html401/present/styles.html#h-14.3.2).  Setting a title on a stylesheet link does a couple of things: it gives the stylesheet “preferred” status on the page (the exact meaning of which is somewhat mysterious, but it seems to enable the stylesheet link to be manipulated), and also provides a way for the browser to group styles so that the user can switch between them using browser controls (i.e., in Firefox, go to View > Page Style—the list you see is the list of titles).

If you add a title attribute and you’re still having problems with it, feel free to shoot us an email.

Comment by Maggie (Filament) on 03/27  at  08:50 PM

@Maggie: Ooof. I always forget about the title attribute on alternative stylesheets. Thanks!

Comment by Travis Forden on 03/28  at  12:14 AM

Have you tested this against Safari 3 on either Tiger or Leopard? I notice this isn’t listed on your matrix. I ask because I was having trouble getting the script to do the jQuery items in ‘enhancedDomReady()’ on first load. When I refresh, they execute. I initially had your script, then the jquery script, then my custom scripts all in separate files. Even when I put them all in a single JS file, I still was having issues. I haven’t been able to isolate the issue. When I remove the test user device script and use the typical $(document).ready() function in jQuery, it works fine. The same issue appears to happen in Opera 9, where the items in ‘enhancedDomReady()’ aren’t executed on first load.

Any ideas why this might happen?

Comment by Travis on 04/06  at  09:33 PM

@Travis:  Good catch, we need to update the matrix with Leopard results.  We’ve tested enhancedDomReady() on Leopard, both in Safari 3 and Opera, and for us it worked in both.  We’ve also tested Safari 2 and Opera 9 in Tiger (listed as Mac OSX 10.4 in the matrix) with the same results, though we’ve yet to test Safari 3 on the older operating system—we’ll put that on our list.

It’s hard to say why the test is failing in your environment, especially when multiple scripts are in play.  In a recent project we noticed that the test failed as you described, but after some debugging realized the problem was not in enhancedDomReady(), but in another one of the scripts we were using.  If you strip away the other scripts and use enhancedDomReady() on its own, does it still fail?

Comment by Maggie (Filament) on 04/07  at  05:04 PM

I think you meant to write “standard compliant” rather than “standard complaint” browsers… or not

Comment by Eric Ongerth on 05/16  at  03:50 AM

About CSS goodies being disabled if browser has Javascript disabled: maybe playing with alternate stylesheets could yield some increased capabilities? For example,
- main stylesheet: ‘basic’ would use safe (as in CSS1) values
- alternate stylesheet: ‘full’ would in fact duplicate the experience of the full product.
Then, add a noscript tag saying “Javascript is disabled. If you are confident your browser can handle it, please use the ‘full’ style sheet”.

Second solution, built upon the above, would be to make said noscript tag contain an hyperlink that would reload the page with a GET setting (or modify a session cookie, whatever...) which would reverse stylesheets priority (this would require minimal server side scripting, thus the first static solution).

This would allow people browsing with, say, Firefox 3 + NoScript (which is not uncommon) a cheap way to appreciate the (almost) full site experience.

The biggest problem would be with browsers that cumulate alternate stylesheets with the main one, and browsers that merely ignore them would see no change.

Comment by mitch074 on 08/19  at  11:48 AM

I have just implemented your script as I have been looking for a good way to test the level of CSS support for ages. However IE 7 and 6 are both failing the first test and returning 20 for the offset width when it should be returning 40.

The section of the code is:

var newDiv = document.createElement(’div’);
document.body.appendChild(newDiv);
newDiv.style.visibility = ‘hidden’;
newDiv.style.width = ‘20px’;
newDiv.style.padding = ‘10px’;
var divWidth = newDiv.offsetWidth;
alert("divWidth = “ + divWidth);
if(divWidth != 40) {
document.body.removeChild(newDiv);
return false;
}

I have tested this in IE 7 and 6 and it only fails on this first test. It seems to work in all the other latest browser version (Safari, Chrome, Opera, Firefox) okay. I can see from your chart that its supposed to work in these browsers so am I missing something? Is this a bug and if not then is anyone else having the same issue and if so what is causing it?

Thanks

Comment by Rob Reid on 11/06  at  04:54 AM

Forget that last comment I just realised why it was happening. IE 6/7 only reports offsetWidth to be equal to padding + width in standards mode whereas the other browsers work either way. My test page did not have a doctype defined and that was causing the difference. With a doctype its working fine.

Comment by Rob Reid on 11/06  at  05:15 AM

@Rob Reid: Thanks for the followup. Good to hear it’s working for you. Judging by your code sample, it looks like you are using the latest version of the script (hosted at A List Apart), but I wanted to check and make sure. We will do an update to this article soon that will document the new version’s API.

Comment by Scott (Filament) on 11/06  at  05:51 PM

The code I used and tweaked for my own site is from this page http://72.47.209.59/examples/testUserDevice/testUserDevice_UNPACKED.js

I am not too hot on CSS but can I ask whether the “Advanced” CSS setting as apposed to the “basic” means that the box model is supported or is it more than that. I have seen tests for the box model where they just check the document.compatMode. Also can I ask whether this has any relation to the various doctype settings.

Thanks

Comment by Rob Reid on 12/08  at  02:46 PM

Amazing work, guys!
This blog is becoming increasingly a source of reference for solutions to my questions!
Thanks.

Comment by Perder Peso on 10/21  at  04:42 AM

The pdf is great. Good work! Thanks!
ı love you..

Comment by porn on 11/21  at  07:55 PM

Hey thanks a lot for the matrix. It is really awesome how you put every browser and their results on the grid.  Thank you.

Comment by Peter on 05/17  at  05:46 PM

Commenting is closed for this post.

Book cover: Designing with Progressive Enhancement

Enjoy our blog? You'll love our book.

For info and ordering: Visit the book site