Gracefully ignoring old browsers

New front-end techniques popping up all the time. Because these changes are moving very fast, supporting old browser might seem tedious. But there are some techniques to make this easier. To achieve this, you need to accept that webpages do not need to look the same in every browser. For example, responsive designs have a different layout on smartphones than on desktops devices. So this is quite common.

The goal is to fail gracefully, meaning that pages should be usable in any browser. You can follow three strategies for old browsers, that build on each other.

  • Progressive enhancement.
  • Ignore optional functionality.
  • Fallbacks.

With progressive enhancement, you serve HTML to all browsers. On top of this, you add CSS. Browsers will ignore HTML and CSS that they don't understand. Advantage is that you can use new features without breaking old browsers.

In CSS you can use the cascade (the C in CSS) to serve different properties. For example, you can give articles a solid background colour. In newer browser you overwrite this with a rgba value. Old browsers will ignore rgba values if they don't understand it, keeping the previous value that serves as a fallback.

<style>
body {
  background: #026873;
}
article {
  background: #04E7FF;
  background: rgba(255, 255, 255, 0.4);
}
</style>

In CSS it also possible to test if a property is supported with @supports. This way you can serve a set of different properties to newer browsers.

<style>
body {
  background-color: #026873;
}
@supports(background-size: 13px) {
  body {
    /* Source: https://leaverou.github.io/css3patterns/#cicada-stripes */
    background-image: linear-gradient(90deg, rgba(255,255,255,.07) 50%, transparent 50%),
    linear-gradient(90deg, rgba(255,255,255,.13) 50%, transparent 50%),
    linear-gradient(90deg, transparent 50%, rgba(255,255,255,.17) 50%),
    linear-gradient(90deg, transparent 50%, rgba(255,255,255,.19) 50%);
    background-size: 13px, 29px, 37px, 53px;
  }
}
</style>

With JavaScript (JS) you have to be a bit more careful. If a browser doesn't understand a piece of JS, it will immediately stop. Any JS code after that point will never be executed.

To use new JS features without breaking old browser, the BBC came up with cutting the mustard technique. This is used to only execute JS that browsers understand by detecting browser features. Old browser will just get static HTML. Of course you will have to decide what functionality can be skipped. In this case, JS is an optional extra to improve the user experience for capable browsers, while serving core content to all browsers. This strategy works well for content sites, but may be less appropriate for highly interactive web applications.

<script>
if ('querySelector' in document
  && 'localStorage' in window
  && 'addEventListener' in window) {
    // bootstrap the javascript application
}
</script>

Here is an example of what you can do with this technique. If you have a blog or news site, you can list the articles with a date. Old browser get a regular formatted date. In newer browsers you can use a different representation, like how many days ago the article was published.

<script>
if ('querySelectorAll' in document && 'addEventListener' in window) {
  window.addEventListener('DOMContentLoaded', function () {
    var now = Date.now()
    var dayInMs = 1000 * 60 * 60 * 24
    document.querySelectorAll('article time').forEach(function (element) {
       var itemTime = Date.parse(element.getAttribute('datetime'))
       var days = Math.floor((now - itemTime) / dayInMs)
       element.innerHTML = days + ' days ago'
    })
  })
}
</script>

With the "cutting the mustard" technique, you can change browser support over time by updating the feature detection. You can raise the bar from IE8 to IE10 by making the cut with window.matchMedia.

<script>
if ('matchMedia' in window
  && window.matchMedia('(min-width: 1366px)').matches
) {
  // Only load JavaScript on large screens
}
</script>

Or set the bar at IE11 by using the Internationalization API.

<script>
if ('Intl' in window) {
  // load JavaScript
}
</script>

You can raise the bar even higher by providing simple fallbacks. For example  the IntersectionObserver API not supported by IE, but available in MS Edge or the latest Safari (12.2). This can be used to lazyload images in recent browsers. In older browser you would load all images immediately.

<img src="placeholder.png" data-src="fullimage.jpg" class="lazyload" alt="">
<script>
if ('IntersectionObserver' in window) {
  // lazyload images
} else {
  // load all images
}
</script>

Alternatively, you can decide to do this natively. In IE11 the lazyload attribute is available. In Chrome 75 the loading attribute will be introduced. Other browsers will probably follow. Images wil load normally in browsers not supporting either of these attributes. This is progressive enhancement at its best, because over time, more browsers will be supported without changing any code. And it even works without JavaScript.


<img src="fullimage.jpg" lazyload loading="lazy" alt="">

If you like to write modern ES6 JavaScript without transpiling it to ES5, you also can use Javascript modules. In HTML you can define scripts as type module. This will only get executed by browsers that fully support ES6. So for example Microsoft Internet Explorer would be ignored. This makes sense for certain audiences that only use the latest browsers. Or you do an internal project for an organisation and you know only Edge and Chrome are available to employees.

<html class="no-js">
<script type="module">
(function (html) {
  html.className = html.className.replace(/\bno\-js\b/, 'js')
}(document.documentElement))
</script>
<script type="module" src="base.js"></script>
<script type="module" src="module.mjs"></script>

Optionally, you can serve a different set of JS, to older browser only, by adding scripts with a nomodule attribute. Modern browsers will not execute these scripts. Actually old browsers do not understand the nomodule attribute and simply interpret it as a regular script.

<script nomodule>
(function (html) {
  html.className = html.className.replace(/\bno\-js\b/, 'fallback-js')
}(document.documentElement))

// Fallback JS
</script>

So, you can use different strategies to have the broadest possible reach and what makes sense for target audiences. And feature detection should probably be based on features that are actually used by your site or application.