Thinking differently about progressive enhancement

People seem to think that progressive enhancement means more work and designing for the least capable browser. And at the cost of delivering better experiences for people who use new browsers.

To the contrary, progressive enhancement is often less work and enables us to design the best experience using the most cutting edge browser features (if needed) without harming users who use older browsers.

Starting with a problem instead of a solution

Before getting to a solution, let’s work out the what we’re building (the problem).

Most websites, digital products or services consist of links, text, images and maybe videos. But they also supply forms to save, send, delete, edit and create things. None of this needs JavaScript which is good because we can avoid the complexity that embodies client-side JavaScript.

For all these things, make sure you send semantic HTML (and CSS) from the server. This way, users get a really fast, standards-compliant and accessible experience that works in every browser—past and future.

Go nuts: pick a great templating library and perhaps a CSS preprocessor because these technological choices don’t affect the user experience—they just affect the developer experience—so we’re all good.

Richer interfaces

With the overwhelming majority of user needs covered, we can look at situations where users might benefit from richer interfaces. On the simpler end of the spectrum, this probably means hiding things on a screen via menus, tabs and accordions etc.

On the more complex end of the spectrum, we could, for instance, let users annotate a web page in realtime with comments being saved and pushed to all collaborators automatically—like Google Docs.

Either way, we’ll need JavaScript.

Using JavaScript APIs

You’d be forgiven for assuming this is where it gets complicated. And it could if you’re not careful. Most of the industry seems to jump on the most popular framework of the moment (and it is a moment). But this is usually a mindless decision and one that’s hard to undo.

Instead, let’s look at what’s needed to design richer interfaces. Spoiler alert: not much.

Find elements: el.getElementById, el.querySelector[All], el.getElementsByClassName

Change element attributes/properties: el.setAttribute, el.getAttribute, el.classlist.*, el.propName =

Respond to events such as click, drag, submit and changes in viewport size: el.addEventListener, window.matchMedia

Modify elements: el.innerHTML, el.appendChild, el.removeChild, document.createElement

Make AJAX calls: XMLHttpRequest

Native helpers: Function.prototype.bind, Array.prototype.forEach, Array.prototyp.map

Some of these APIs work “everywhere” and some work only in modern browsers. But it doesn’t matter. The web is a continuum and so no matter the API we’re using we need to approach it the same way because thinking in terms of browsers is Sisyphean and unproductive.

Feature detection: the technical solution to the progressive enhancement philosophy

If we stop thinking in browsers, we only have to think in features. And once we do that we need to make sure that the feature works (in whatever set of browsers) before we attempt to enhance an interface. This is called feature detection.

That’s all a bit philosophical but that’s because progressive enhancement is philosophical. Let’s turn this into a practical example. We’ll pseudo code a component that shows some stuff when a button is clicked.

Of course, we’ve already done much of the hard work by server-side rendering the stuff with with semantic HTML (and CSS to make it readable):

<div class="stuff">
  <div class="stuff-content">
    <p>Stuff to show/hide</p>
  </div>
</div>

First, the script needs to create a button:

var button = document.createElement('button');
button.type = "button"
button.textContent = "Show content";

Then it needs to be added to the container:

var container = document.querySelector('.stuff');
var content = document.querySelector('.stuff-content');
container.insertBefore(button, content);

Then we want to listen to the click event on the button and toggle the class name and button state to show and hide the content.

button.addEventListener('click', function() {
  if(content.classList.contains('hidden')) {
    content.classList.remove('hidden');
    button.textContent = "Hide content";
  } else {
    content.classList.add('hidden');
    button.textContent = "Show content";
  }
}, false);

At this point this code works in browsers where all the following APIs are supported:

But if just one of these APIs aren’t supported (in a particular browser) the component breaks. So we need to feature detect them all before using them.

To helps us do this, we can use two little functions which Peter Michaux sets out in Feature Detection: State of the Art Browser Scripting:

function isHostMethod() {
  var objectMethod = object[method];
  var type = typeof objectMethod;
  return type == 'function' || type == 'object' && null !== objectMethod || type == 'unknown';
}

function isHostObjectProperty() {
  var objectProperty = object[property];
  return typeof objectProperty == 'object' || typeof objectProperty == 'function' && null !== objectProperty;
}

And we can use them like this at the top of the script:

if(isHostObjectProperty(document.body, 'textContent')
  && isHostMethod(document, 'createElement')
  && isHostMethod(document, 'querySelector')
  && isHostMethod(document.body, 'insertBefore')
  && isHostMethod(document.body, 'addEventListener')
  && isHostObjectProperty(document.body, 'classList')) {
  // put the rest of the code inside here.
}

If a browser doesn’t support any of the APIs, the user always sees the content. It works, but it’s just not enhanced using progressive disclosure. Importantly, we’ve made sure the experience isn’t broken for anyone. That’s inclusion.

Libraries aren’t redundant

What if we have lots of components in our application? And what if these components use some of the same APIs and therefore require the same feature detection code? Who wants to write that same, long-winded code over and over? Nobody.

This is where a library of functions comes in handy (that’s all a library is by the way). One recent idea touted by the JavaScript community is that libraries are no longer needed, but that’s a misconception.

With a bit of thought—something that Peter Michaux sets out in Cross Browser Widgets—our code could look like this:

if(lib.hasFeatures(
  'setText',
  'querySelector',
  'createElement',
  'prependElement',
  'addEventListener',
  'addClass',
  'hasClass',
  'removeClass')) {
  var button = lib.createElement('button', {
    type: 'button', text: 'Show content'
  });
  var container = lib.querySelector('.stuff');
  var content = lib.querySelector('.stuff-content');
  lib.prependElement(button, content);
  lib.addEventListener(button, 'click', function() {
    if(lib.hasClass(content, 'hidden')) {
      lib.removeClass(content, 'hidden');
      lib.setText(button, 'Hide content');
    } else {
      lib.addClass(content, 'hidden');
      lib.setText(button, 'Show content');
    }
  });
}

Do it once, and never have to do it again. And such a library weighs in at a few kilobytes. Cutting edge, and inclusive all at the same time. If a new browser comes out fine, if an old browser becomes obsolete, fine too.

There’s also nothing to stop you creating further abstractions. Perhaps event delegation is useful. Or maybe you want to create reactive (that’s the buzzword these days right?) components.

Build on top of a library of feature detected functions and everyone benefits—users and developers.

Less checking caniuse.com. Less testing in multiple browsers. Less worrying about excluding users. Less worrying about using older technology. Less worry about wrangling with [insert popular framework here]. Less worrying about new browsers. No more polyfills and their unavoidable caveats. No more worrying about whether JavaScript is available or not.

That sounds like a better experience for users and a better time for developers who get to use cutting edge APIs responsibly. And that's something you can put on your CV.

I write articles like this and share them with my private mailing list. No spam and definitely no popups. Just one article a month, straight to your inbox. Sign up below: