Skip to content

Dialog for Unsupported Browsers

There is no greater failure than showing a white screen. If an application can't run in a given environment ("old" browsers) telling users might not make them upgrade right away. But compared to a blank page they'll actually know what's going on. Here's a (debatable) method to achieve that.

My day job is comprised of building web applications. Without bothering you with the details, our lowest end browsers are Internet Explorer 9 and Android 4.0.x Stock Browser. We chose to not support IE8 at all. This decision allowed us to go with ES5, SVG, CSS Transform etc. without having to polyfill anything. On top of using these technologies, we created BackboneJS applications which made our HTML pretty much look like <body></body>. There are reasons we don't render any HTML on the "server", but I'm not at liberty to discuss this any further at this point.

Of course all this lead to a blank blank page in old browsers. If that wasn't enough, the browsers would throw a myriad of JavaScript errors because it couldn't parse and or execute anything we shoved down their throats.

The Management™ agreed with the developer's pride: We needed some sort of "beautiful" masquerade for this.

The Or Else Approach

Our first draft looked a bit like the following snippet. We inlined the content targeting old browsers into our primary HTML files and had JS overwrite the content if necessary. To prevent old browsers from parsing and executing our JavaScript we only injected it if we were sure the browser could run it.

<head>
  <link rel="stylesheet" href="style.css">
</head>
<body>
<script type="javascript/template" id="old-browser-html">
  <p>Your Browser sucks!</p>
</script>
<script type="javascript/template" id="modern-browser-js">
  <script src="main.js"></scr-ipt>
</script>
<script>
  if (oldBrowser) {
    // remove any existing CSS
    var links = document.getElementsByTagName('link');
    while (links.length) {
      links[0].parentNode.removeChild(links[0]);
    }
   
    // inject old browser dialog
    var html = document.getElementById("old-browser-html");
    document.body.innerHTML = html.innerHTML;
  } else {
    // load primary JS
    var script = document.getElementById('modern-browser-js');
    script = script.innerHTML.replace(/<\/scr-ipt>/g, '</script>');
    document.body.insertAdjacentHTML("beforeend", script);
  }
</script>
</body>

I have cut down the document to the core ingredients and even omitted how we determine if oldBrowser is true or false for brevety sake. We're using the script template method (<script type="javascript/template">) to carry HTML fragments in our document that are not parsed as HTML, CSS or JavaScript. Note that within such a script template we had to "tweak" the closing script tags from </script> to </scr-ipt> in order to have the stuff parsed properly.

In case we are dealing with IE8 (oldBrowser = true), we have to remove any stylesheets that the document references itself. We then simply replace the document's <body> with the content of the template for old browsers.

In case we are dealing with a modern browser, we have to kick off loading the JavaScript. Because we were using grunt-usemin the <script> couldn't just be added to the DOM like you normally would:

var script = document.createElement('script');
script.src = 'main.js';
document.head.appendChild(script);

While this technique worked in the beginning, it has always suffered a couple of problemes and failed us completely in the end.

  • The HTML for old browsers is always transmitted to the client, although it is almost never rendered. Considering that our HTML is never cached, this seemed like a waste.
  • The JavaScript couldn't be identified and loaded by the pre parser - which we could've "fixed" in Chrome using <link rel="subresource" href="script.js">.
  • The fact that we had to rewrite the closing script tag (to </scr-ipt>) never made anyone smile

The approach really fell apart once we added inline <svg> (with more <script>s for animations). We were back to the blank page we saw without all this fuss.

The Unless Approach

We were getting close to the release date (In fact we were already 3 days past delivery, but you know how that works). I was handed this bug. I had no time (4 hours till I'd leave for Fronteers and not return in time for another round of bug fixing).

Lanyrd's mobile site uses the following trick to conditionally load scripts:

<script>preventScript && document.write("<!"+"--")</script>
 <script src="somes-script.js"></script>
<!---->

Which works fine if the content you're trying to escape doesn't contain any HTML comments. My content did, so that trick wasn't going to fly for me. But I remembered reading about a hack that allowed JavaScript to take over a document's content before the browser's parser had a chance to work its magic - MobifyJS. They inject a <plaintext> element, by which the parser exits HTML mode and simply treats everything that follows as plain text content of the <plaintext> element.

The <plaintext> element is deprecated since HTML2 - which became an IETF standard in 1995. MDN clearly marks this element as obsolete, yet MozHacks (implicitly) promotes the use of <plaintext> as late as March 2013 - nearly two decades after its demise.

After testing the support of <plaintext> on the various browsers and devices available to me, I decided to go forward with using it. My final run the app or GTFO hack looks simple enough in the document:

<body>
  <script>
    if (oldBrowser) {
        document.write('<scr'+'ipt src="old-browser-dialog.js"></scr'+'ipt>');
    }
  </script>
  <!-- actual content -->
</body>

and the old-browser-dialog.js looks a little something like this:

(function() {
  var html = '<link rel="stylesheet" href="old-browser-css.css">'
    + '<p>Your Browser sucks!</p>';

  // clean existing styles
  removeElements('link');
  removeElements('style');

  // add old browser dialog
  document.write(html);

  // prevent document's content from being evaluated
  document.write('<plaintext style="display: none">');
     
  function removeElements(tagName) {
    var nodes = document.getElementsByTagName(tagName);
    while (nodes.length) {
      nodes[0].parentNode.removeChild(nodes[0]);
    }
  }
})();

That solved all the problems we had.

  • Presenting old browsers an update dialog.
  • Preventing them from executing our JavaScript.
  • Being unobtrusive to the rest of the document's content.

Once I committed this to our SVN, I called my team to my screen and explained that I would shoot anyone that ever

  1. Uses document.write()
  2. Uses deprecated HTML Elements
  3. Question my ability to break the web

(yeah yeah, the irony, I know…)

Comments

Display comments as Linear | Threaded

No comments

The author does not allow comments to this entry