Ready() for the new era

September 4th, 2025

Back in the day when jQuery ruled the roost, we would use the ready handler to make sure our code ran after the DOM and stylesheets had completely loaded.


$( document ).ready(function() {
  // Handler for .ready() called.
});

(Or, more likely, the recommended version.)


$(function() {
  // Handler for .ready() called.
});

But of course we don’t rely on loading jQuery in our head anymore. But nobody wants to write out the full logic manually.


if (document.readyState != "loading") {
  bootstrap();
} else {
  document.addEventListener("DOMContentLoaded", bootstrap, false);
}

But if we’re loading our code asynchronously, we kind of have to. Some people went as far as to synchronously load their JavaScript right before the </body> tag. That will make sure everything is ready when it runs, but at the cost of loading all your JavaScript later.

But there is a nice, short boilerplate that you can use that makes it easy to trigger on ready:


const ready = new Promise((resolve, _) => {
    if (document.readyState !== 'loading') resolve();
    else document.addEventListener('DOMContentLoaded', resolve);
});

Slap that basically anywhere and you can then run ready.then( ... ) anywhere in your code base that you can see it. And you don’t need to be careful about making sure there’s only one declaration. (Unless you have dependencies on initialization order.) A short handful of lines to give you a more fluent syntax for scheduling things on ready.

And unlike the old jQuery version, you don’t need to load it synchronously before everything else.


Incompetent Browsers: Building the Fallback Experience

August 11th, 2025

Previously: Browser Grading, Historical Browser Detection, Requirements and Loaders, Detection and Fallback, The Complete Solution, Goldplating, Dijon Mustard.

Remember when we talked about browser grading, Yahoo said:

C-grade is the base level of support, providing core content and functionality. It is sometimes called core support. Delivered via nothing more than semantic HTML, the content and experience is highly accessible, unenhanced by decoration or advanced functionality, and forward and backward compatible. Layers of style and behavior are omitted.

What does that look like in 2025?

Assumptions

Start by building your site mobile-first. The most important part of this is having a sensible order to your HTML, so that someone reading it from top to bottom can have a reasonable experience. You should also be setting your image sizes using CSS. In 2025, you probably are already doing both of these things, but if not, fix them.

What about my site navigation?

The first hurdle you’re going to hit is your site navigation. Almost all sites have navigation in their headers, and maybe they tuck it off to the side on mobile under a hamburger menu toggle, but it’s still there in the HTML, where people will have to scroll past it to get to the content. Luckily, there’s a pure HTML solution that handles this and improves your site too.

Put a “skip navigation” link as the first thing in your page, right after the opening body tag. With the appropriate styles it will be invisible to most users, while still appearing if anyone is trying to navigate the page with their keyboard or a screen reader. For people in the fallback experience, they can skip the large nested lists that have links to every page on your site and go right to the content. Just make sure you put id=main on the tag where your content actually starts.


<div class="skip-navigation">
  <a href="#main" class="button">Skip to main content</a>
</div>


.skip-navigation {
  a {
    position: absolute;
    top: 0;
    transform: translateY(-100%);
    transition: transform .2s ease-out;

    &:focus {
      z-index: 100000;
      transform: translateY(0);
    }
  }
}

The image problem

At this point you’ll look at your pages and see that you’re actually shipping arbitrarily sized images which don’t really flow right with your content. And if you check the fallback experience on mobile, they probably are wider than the phone screen.

The answer is: set a width of 320 in the HTML.

Why 320? That’s the pixel width of the original iPhone, so any smartphone will be able to display it without horizontally scrolling. If you don’t set the height, it will scale while maintaining the aspect ratio. If you’re setting image sizes with CSS in your normal presentation, it will override the size set in the HTML attribute.

When I built this, we were using WordPress. This code got us most of the way there. (We had some custom Gutenberg Blocks that also needed to be updated to generate the right widths.)


class Image_Utils
{
    public static function get_image_fallback_sizes($id)
    {
        // Default to empty strings so fallback sizes are ignored if unset.
        $width  = '';
        $height = '';

        $meta = wp_get_attachment_metadata($id);

        if ($meta) {
            $width  = $meta['width'];
            $height = $meta['height'];
        }

        if (empty($width)) {
            // SVG files do not get size metadata.
            $width = 320;
        }

        if ($width > 320) {
            $scale  = 320 / $width;
            $width  = (int) ($width * $scale);
            $height = (int) ($height * $scale);
        }

        return array($width, $height);
    }

    public static function rewrite_image_tag($id, $html)
    {
        list($width, $height) = self::get_image_fallback_sizes($id);

        $html = preg_replace(
            '/width=[\'"]?\d*[\'"]?/',
            'width="' . $width . '"',
            $html
        );
        $html = preg_replace(
            '/height=[\'"]?\d*[\'"]?/',
            'height="' . $height . '"',
            $html
        );

        return $html;
    }
}


function post_thumbnail_html($html)
{
	$id = get_post_thumbnail_id();
	return Image_Utils::rewrite_image_tag($id, $html);
}
add_filter(
	'post_thumbnail_html',
	'post_thumbnail_html'
);

Browse happy

Since we’re here, we might as well tell them to update their browser. Put this right after the skip link.


<p class="browserupgrade">
  You are using an <strong>outdated</strong> browser. Please
  <a href="https://browsehappy.com/">upgrade your browser</a> to
  improve your experience and security.
</p>

Fallback styles and scripts?

I don’t think you should use fallback styles or scripts. Doing so means you now have three versions of your site to test. But you can. If you do, here are some hints:

For CSS, imagine you’re targetting IE6, or Netscape 4. No CSS3 Selectors. No Grids. No Web Fonts. SCSS Features are okay though; That’s build-time

For JavaScript, dig out the old addEvent fallback that registered events appropriately in IE. Don’t forget to specify the third parameter as false when you call addEventListener; Opera doesn’t like it if it’s omitted. Always check for the existence of any API you call. You probably won’t have QuerySelectorAll and will have to stick with getElementsByClassName. Remember to register events for load, as not all browsers have DOMContentLoaded. And Test, test, test.


Incompetent Browsers: Dijon mustard

August 6th, 2025

Previously: Browser Grading, Historical Browser Detection, Requirements and Loaders, Detection and Fallback, The Complete Solution, Goldplating.

Right now, we’re using the presence of dynamic imports to determine if our browser is good enough. But those became available in 2019! Maybe we’re using something newer than that, like container queries, which only became available in 2023. Let’s be honest, none of us test against Chrome 63, when dynamic imports appeared. We should really have a much stronger test.

Well, good news. Our test is just JavaScript, so we can place our cutoff wherever we want.

2021

In 2021, Chrome and Firefox added support for using a list of css selectors inside :not(). Safari had supported it for years. Meanwhile, in 2020, Chrome and Safari added support for CSS3 Image Orientation, which Firefox had supported for years. If we test both of these, we’ll get a test that only passes browsers with both features, leaving us with a browser that was relatively modern in 2021.

Let’s add this CSS to our head


<head>
  <style>
    head:not(a, p, b, i) {
      image-orientation: from-image;
    }
  </style>
  <script></script>
</head>

This won’t actually change any styles. It only styles the head tag itself. Then we can check if it worked correctly with a simple line of JavaScript:


if (getComputedStyle(document.head).imageOrientation == 'from-image') {
}

2022

For 2022, we’ll use the same technique, but with border-start-start-radius, which became supported in 2021 in Chrome and Safari, but in 2019 for Firefox, and system-ui as a font, which only started working correctly in Firefox in 2021.


head {
    border-start-start-radius: 12px;
    font-family: system-ui;
}


var cs = getComputedStyle(document.head);
if (cs.borderStartStartRadius === "12px" && cs.fontFamily === "system-ui") {
}

2023

In 2022 and 2023, container queries came online pretty quickly in all browsers.


<head style="font-size: 1cqw;">


if (document.head.style.fontSize === "1cqw") {
}

2023, but picky

For some reason in 2023, I also prepared an alternate test that only allowed browsers updated in 2023, not just recently. It used historical font forms, attachInternals, and import map support to make sure all three major engines had updated. And it still tested for container queries. I’m not sure why I did this one, to be honest.


<head style="font-size: 1cqw">
  <meta charset="utf-8" />
  <style>
    head {
      font-variant-alternates: historical-forms;
    }
  </style>
  <script></script>
</head>


var cs = getComputedStyle(document.head);
// Safari Test: import maps 16.4+
// var im = HTMLScriptElement.supports && HTMLScriptElement.supports('importmap');
var ai = !!document.head.attachInternals;
// Firefox Test: Container query Units 110+
var cq = document.head.style.fontSize === "1cqw";
// Chrome Test: fontVariantAlternates 111+
var fva = cs.fontVariantAlternates == "historical-forms";

if (ai && cq && fva) {
}

Looks like I decided not to use the import maps test after all. Whatever.

2024

In 2024, Chrome 120 added CSS :dir() support, Firefox 126 added zoom support, and Safari 17.0 added support for modulepreload hints. Safari 17.5 added support for balanced text wrapping.

Note that these are all still Baseline Newly Available, so this is probably a little too new to rely on.


<head dir="rtl" style="zoom: 150%; text-wrap:balance;">


var dir;
try {
  dir = document.querySelectorAll(":dir(rtl)");
} catch {
  dir = [];
}
var link = document.createElement("link");
link.relList.supports("modulepreload");

if (
  document.head.style.zoom === "150%" && // FF 126+
  // && document.head.style.textWrap === 'balance' //Safari 17.5+
  dir.length && // Chrome 120+
  link.relList.supports("modulepreload")  // Safari 17.0+
) {
}

2024 – What We Do in the Shadows

In 2023 and 2024, declarative Shadow DOM came online. At the time we decided it wasn’t ripe enough to rely on.


// Declarative Shadow DOM
// Chrome 111+, Safari 16.4+, FF 123+
if (HTMLTemplateElement.prototype.hasOwnProperty("shadowRootMode")) {
}

Real nice test though. Especially if you’re using Web Components and Declarative Shadow DOM and need to send non-supporting browsers to the fallback experience anyway.


Incompetent Browsers: gold plating the CSS loader

August 5th, 2025

Previously: Browser Grading, Historical Browser Detection, Requirements and Loaders, Detection and Fallback, The Complete Solution.

We have a good quality solution, and I recommend that you stick with it. But what if we went a little crazy?

Madness

Our solution has a problem: it uses document.write(). And if you’re using Lighthouse to evaluate your page quality, this will ding you. (I think this is overblown. The primary reason people want you to avoid doocument.write() is because of bad adtech snippets that synchronously load JavaScript, blocking parsing for the entire download time. That’s not what we’re doing here, but the advice is overtuned.) But sometimes your boss tells you to get rid of it so you get a clean lighthouse score.

So we have another alternative. We can load the CSS asynchronously, and block rendering ourselves.

Now if we just wrote a normal HTML page, We’d link the CSS in the head and the JavaScript at the end of the body, meaning that when the JavaScript loads, the DOM is completely built and the CSS has all been loaded. If we loaded our JavaScript asynchronously, we could schedule our activity for DOMContentLoaded or even for load if we needed them. But when we asynchronously load our CSS and our JavaScript, we’ll need some other event to rely on.

So we can change our loader to fire off the events that we want!


var target = document.getElementsByTagName("script")[0];
var head = target.parentNode;

function hideDocumentUntilLoaded() {
  function bootstrap() {
    document.readyforscripting = true;
    var e = new CustomEvent("readyforscripting");
    document.dispatchEvent(e);
  }
  var hide = document.createElement("style");
  hide.innerText = "body {display:none;}";
  head.insertBefore(hide, target);
  document.addEventListener(
    "stylesloaded",
    function () {
      head.removeChild(hide);
      if (document.readyState != "loading") {
        bootstrap();
      } else {
        document.addEventListener("DOMContentLoaded", bootstrap, false);
      }
    },
    false
  );
}

var waiting = {};
var complete = {};

var handler = {
  handleEvent: function (event) {
    complete[event.target.href] = event.type === "load";
    delete waiting[event.target.href];
    var size = 0;
    for (var key in waiting) {
      if (Object.hasOwnProperty.call(waiting, key)) {
        size++;
      }
    }
    if (size === 0) {
      try {
        var e = new CustomEvent("stylesloaded", { detail: complete });
        document.dispatchEvent(e);
      } catch (e) {
        if (
          e.name === "TypeError" &&
          e.message === "Object doesn't support this action"
        ) {
          // swallow error from IE 9-11 CustomEvent is not new-able
        } else {
          throw e;
        }
      }
    }
  },
};
function insertStyle(src) {
  var ss = document.createElement("link");
  ss.setAttribute("rel", "stylesheet");
  ss.setAttribute("href", src);

  if (ss.addEventListener) {
    ss.addEventListener("load", handler, false);
    ss.addEventListener("error", handler, false);
    waiting[ss.href] = true;
  }

  head.insertBefore(ss, target);
}

insertStyle("main.css");
insertStyle("more.css");
insertStyle("evenmore.css");
if (document.addEventListener) hideDocumentUntilLoaded();

As before, we hide the document with a style tag which we remove once the real CSS is loaded. But now we track which link tags are in progress and only remove it once they’re all done. (We use handleEvent here so we can process on both successful load and error.) When all the styles have loaded or failed, we fire a stylesloaded event.

When both stylesloaded and DomContentLoaded have fired, we fire readyforscripting and set document.readyforscripting, which we can use in our main script files to only start doing DOM manipulation when we’re actually ready. IE doesn’t let us make custom events, so those events don’t trigger in IE. (Not that you should be loading anything in IE anyway.)


Incompetent Browsers: a complete, modern solution

August 4th, 2025

Previously: Browser Grading, Historical Browser Detection, Requirements and Loaders, Detection and Fallback.

Code we can be reasonably happy with

Here’s a complete detector/loader:


<link rel="preload" as="style" href="/styles/core.css" />
<link rel="preload" as="style" href="/styles/structure.css" />
<link rel="preload" as="style" href="/styles/content.css" />
<link rel="preload" as="script" href="/scripts/bundle.js" />

<script>
  (function (mainStyles, mainScripts, fallbackStyles, fallbackScripts) {
    var target = document.getElementsByTagName('script')[0];
    var head = target.parentNode;
    var mode;

    function insertStyle(src) {
      document.write('<link rel="stylesheet" href="' + src + '">');
    }

    function insertScript(src) {
      var s = document.createElement('script');
      s.setAttribute('src', src);
      head.insertBefore(s, target);
    }

    try {
      if (window.URL && window.URLSearchParams) {
        var url = new URL(document.URL);
        mode = url.searchParams.get('mode');
        if (mode == 'fallback') {
          throw new Error('fallback mode requested');
        }
      }
      if (mode != 'broken') {
        var f = new Function('import("test dynamic imports")');
        for (var i = 0; i < mainStyles.length; i++) {
          var ss = mainStyles[i];
          insertStyle(ss);
        }
        for (var i = 0; i < mainScripts.length; i++) {
          var s = mainScripts[i];
          insertScript(s);
        }
      }
    } catch (e) {
      for (var i = 0; i < fallbackStyles.length; i++) {
        var ss = fallbackStyles[i];
        insertStyle(ss);
      }
      for (var i = 0; i < fallbackScripts.length; i++) {
        var s = fallbackScripts[i];
        insertScript(s);
      }
    }
  })(
    ['/styles/core.css', '/styles/structure.css', '/styles/content.css'],
    ['/scripts/bundle.js'],
    ['/styles/fallback.css'],
    ['/scripts/fallback.js']
    /* Don't autoformat this file. If the formatter adds a trailing comma it will break IE */
  );
</script>
<script>/* We need this script to prevent a FOUC */</script>

What’s new in this version? Well, it’s wrapped in a function expression so it’s not making a bunch of global variables. It can load multiple scripts and styles. It can load scripts and styles in the fallback case. (Just make sure they’re written to work in Netscape 4!)

Because we’re allowing scripts and styles in the fallback path, we also support ?mode=broken, which loads nothing at all, so you can check how it will look both IE9 and IE3. (IMO, this is excessive, and I wouldn’t allow fallback scripts or styles if I ran the zoo.)

We’ve also included preload tags for the files that competent browsers want. That means they’ll start downloading right away. This is straight out of a project I was working on which built to a plain script; If your JavaScript is served as a module, make sure to use modulepreload for your module instead.

Note also the two comments near the end. A lot of code formatters like to leave trailing commas on parameter lists, which helps make diffs more readable. This is totally legal in JavaScript, but not in Microsoft JScript. You’ll get a syntax error in IE if you do that.

The other thing is the empty </script> tag at the end. It turns out that Firefox only blocks rendering and waits for stylesheets if you do this. It doesn’t have to be empty, which is why you almost never see it these days, but when you document.write() your stylesheet, there needs to be some script tag to trigger layout blocking.

I spent a lot of time debugging this guy in BrowserStack.


Incompetent Browsers: towards a complete, modern solution, part II

August 2nd, 2025

Previously: Browser Grading, Historical Browser, Requirements and Loaders

Detection

Our basic logic is:


if (browserIsGood) {
  loadJavascriptAndCSS();
}

Let’s dig in to what sits behind that browserIsGood calculation. We’re currently using module support as our test, so a simple version would be:


if (HTMLScriptElement.supports('module')) {
  loadJavascriptAndCSS();
}

Seems perfectly reasonable. Unfortunately, it’s not true. HTMLScriptElement.supports('module') was added much later than actual module support.

We could test for dynamic import support. That’s a lot closer to our real target.


var test = false;
try {
  var f = import("./mod.js").then(
    () => {
      out.innerHTML = "Module Import Succeeded";
    },
    () => {
      // Edge 16 tries to run import, but doesn't actually do so.
      out.innerHTML = "Module Import Failed";
    }
  );
  test = true;
} catch (e) {
  test = false;
}

But this fails our requirement to be compatible with older browsers! It turns out that import is not a function! It’s a reserved keyword and this code will fail with a syntax error in older browsers. But there is a tool we can use to do a syntax check at runtime: new Function.


var test = false;
try {
  var f = new Function('import("test dynamic imports")');
  test = true;
} catch (e) {
  test = false;
}

var out = document.getElementById("outcome");

if (test) {
  out.innerText = "Test Passed";
} else {
  out.innerText = "Test fAILED";
}

Interestingly, we don’t have to load a real module or deal with the asynchrony. Older browsers will throw an exception with a syntax error, while newer browsers will pass, but the function f is never actually executed!

This code also fails our requirements to be compatible with older browsers, but in a much smaller window. It fails in IE4, because IE4 doesn’t support try. But IE4 will still behave the way we want, because we don’t want to do anything in IE4, and having our JavaScript not run because of a syntax error is a form of not doing anything.

Testing the fallback mode

One of our requirements is that we be able to enter our fallback mode deliberately for the purpose of testing.


if (window.URL && window.URLSearchParams) {
  var url = new URL(document.URL);
  mode = url.searchParams.get("mode");
  if (mode == "fallback") {
    throw new Error("fallback mode requested");
  }
}

This code lets us add ?mode=fallback to our URL and see what it will look like in a browser that doesn’t pass the test. That’s useful for developing your fallback experience


Incompetent Browsers: towards a complete, modern solution, part I

July 30th, 2025

In Part One, we looked at the historical practice of browser grading. In Part Two, we looked at some historical solutions for browser detection. Today, we’re going to start putting it together.

Requirements

Let’s start by thinking about browser support. A lot of our historical solutions came from a world where ongoing support for IE was an issue; thank goodness we don’t live there anymore. Our biggest danger these days is someone coming to our site with an old phone that has a several years old version of webkit or chromium. Especially if they are using an app-hosted browser that uses a system webview.

There are 3 browser engines commonly in use, and 3 obsolete browser engines. These are our browser grades:

A-grade experience: If someone comes to the site with an up-to-date browser running Gecko, Blink, or Webkit, we want to give them the full experience.

Obsolete C-grade experience: If someone comes to the site with an out-of-date browser running Gecko, Blink, or Webkit, we want to give them the plain HTML experience.

Archaic C-grade experience: If someone comes to the site with a browser running Trident (IE,) Spartan (Old Edge,) Presto (old Opera,) or some other ancient thing (Netscape? iCab? Lynx?) we want to give them the plain HTML experience.

It’s probably worth the time to add a few more functional requirements:

Opt-In: We’ll have to develop a plain HTML experience for our site. It would be nice to be able to develop this in a modern browser, so we should be able to opt-in to the plain HTML experience for development.

ES3/Jscript Compatible: If we write any code that decides if we should load our JavaScript and CSS, that code will have to run successfully in any browser. Not just any modern browser; any browser. This means limiting ourselves to EcmaScript version 3, and also to the limitations of the not-quite compatible JScript. This code will be written like it’s 1999.

Let’s write some code

The simplest answer is to load our full experience with a module tag, and let it load everything.


<script type="module" src="./main.js"></script>

And maybe you can get away with that. But this is likely to leave you with a Flash of Unstyled Content, as your CSS won’t be loaded until your JavaScript runs.

We really want something like this:


if (browserIsGood) {
  loadJavascriptAndCSS();
}

Okay, that’s a simple structure, but there’s a lot of handwaving happening there. Let’s dig in.

Loading JavaScript

Can we just use dynamic import?


if (browserIsGood) {
  import("./main.js");
}

No, we can not! import is a reserved keyword in JavaScript. This will crash if you try to run it in a browser that doesn’t support dynamic import. We’ll have to load it the old fashioned way.

Everyone knows how to do this by now, right?


var target = document.getElementsByTagName("script")[0];
var head = target.parentNode;

function insertScript(src) {
  var s = document.createElement("script");
  s.setAttribute("src", src);
  s.setAttribute("type", "module");
  head.insertBefore(s, target);
}

if (browserIsGood) {
  insertScript("./main.js");
}

Here we use the first script tag as the target for inserting, because the page might not have a head tag, and we create a script tag dynamically. Our JavaScript will be loaded asynchronously, so we’ll need to wait for DOMContentLoaded before we do anything. But these are table stakes for a modern web site these days. No big deal.

Loading CSS

So how can we load CSS dynamically without causing a FOUC?


var target = document.getElementsByTagName("script")[0];
var head = target.parentNode;

function insertStyle(src) {
  document.write('');
}

if (browserIsGood) {
  insertStyle("./main.css");
}

Wait, really? document.write? Yes. I’m sorry, but if you want to prevent a FOUC, you need there to be a link tag in the head before the parser sees the body. Browsers will block rendering in that case, and will not block rendering, leading to a FOUC in every other case.

Unless you want to get a little crazy and do it all yourself.


var target = document.getElementsByTagName("script")[0];
var head = target.parentNode;

var hide = document.createElement("style");

function hideDocument() {
  hide.innerText = "body {display:none;}";
  head.insertBefore(hide, target);
}

function handler() {
  head.removeChild(hide);
}

function insertStyle(src) {
  var ss = document.createElement("link");
  ss.setAttribute("rel", "stylesheet");
  ss.setAttribute("href", src);

  if (ss.addEventListener) {
    ss.addEventListener("load", handler, false);
    ss.addEventListener("error", handler, false);
    waiting[ss.href] = true;
  }

  head.insertBefore(ss, target);
}

if (browserIsGood) {
  hideDocument();
  insertStyle("./main.css");
}

Here we add a style tag that hides the body tag, and then we remove it once the stylesheet loads (or fails to load). Note that we only call hideDocument once we know we’re inside a good browser.


Incompetent Browsers: Browser Detection

July 29th, 2025

Browser Grading is all well and good, but eventually you have to actually do something different in the browsers you want to get the full experience and the browsers you want to have a simplified experience. Today, we’ll blitz through a bunch of techniques that people have used in the past to do this.

Ultimately, all of these techniques are about Graceful Degradation. There’s a lot of discussion out there about Graceful Degradation and its politer cousin Progressive Enhancement. But to handle truly incompetent browsers, we basically want to turn off all the fancy stuff. Generally speaking, this means you want to load your JavaScript and CSS only if the browser is good enough to use it.

IE Conditional Comments

Good ol’ Internet Explorer has a weird feature called conditional comments that lets you write HTML that only shows in IE, or only shows in non-IE browsers. Of course, when they made IE slightly better, they turned it off so that they would get the good code, but IE 5-9 can be excluded this way.


<!--[if !IE]> -->
According to the conditional comment this is not IE 5-9
<!-- <![endif]-->

CSS Hacks

By taking advantage of known CSS parsing limitations, you can write CSS that will only be read by certain browsers. Be careful that you don’t end up writing two different stylesheets in one file though.

This famous CSS hack sends different widths to older IE browsers and other browsers, which was super important when IE improperly implemented the CSS Box model.


div.content { 
  width:400px; 
  voice-family: "\"}\""; 
  voice-family:inherit;
  width:300px;
} 

Test everything

Instead of checking for browsers, what it we checked for features! Several JavaScript libraries made this convenient, with has.js and Modernizr being the hottest ones back in the day. Modernizr is even still being maintained!

Eventually, the browser rolled these ideas into real features, with the @supports rule, and CSS.supports.


result = CSS.supports("display: flex");

Cutting the mustard

But we don’t really want to test every single feature. We want to know if the browser is capable enough or not. This is really a boolean test. The BBC came up with a test they called Cutting The Mustard, to see if a browser is really modern. The original test passed on IE9+ and Android Webkit, which you probably don’t want to support today. An updated cut removes those two obsolete browsers.

With the rise of modern JavaScript, we can use module support as a mustard test. If the browser supports modules, it gets your JavaScript, and if it doesn’t, it doesn’t. You’ll need to be a little careful with your CSS though.


<script type="module" src="./mustard.js"></script>


Incompetent Browsers: Browser Grading

July 28th, 2025

If you want to build web sites that actually work, you have to take into account the wide variety of browsers that might visit, and take steps to make sure that everyone can use your site. The question is, what are you going to do when someone comes to your site in a browser that doesn’t actually work?

A lot of sites just fail, in that they are completely unusable. But a little forethought can keep the basics of your site usable for anyone. If your site is a full-on app and someone comes by in IE3, there’s nothing to do but tell them to come back with a real browser. But you can at least successfully do that! If you’re building a site for a restaurant, people really only want the address and hours, and you should be able to deliver that to any browser.

One of the earliest systematic attempts to handle this issue was Browser Grading: giving plausible browsers a grade for how good they were, and making sure they got appropriate content. For years, the king of browser grading was Yahoo, or actually, the Yahoo UI Library.

Yui evaluated browsers on 4 criteria:

Identified vs. Unknown There are over 10,000 browser brands, versions, and configurations and that number is growing. It is possible to group known browsers together.
Capable vs. Incapable No two browsers have an identical implementation. However, it is possible to group browsers according to their support for most web standards.
Modern vs. Antiquated As newer browser versions are released, the relevancy of earlier versions decreases.
Common vs. Rare There are thousands of browsers in use, but only a few dozen are widely used.

They then gave browsers one of 3 grades: A, C, or X.

A-Grade browsers were identified, capable, modern, and common. They were identified with a whitelist. They got full QA Testing

C-Grade browsers were identified, incapable, antiquated, and rare. They were identified with a blacklist. They got sparse QA Testing, but bugs were still addressed.

X-Grade browsers were all others. They were unidentified and QA did not test them.

A-Grade browsers got the full functionality of the site. X-grade browsers were assumed to be trying, and got the full functionality too.

C-Grade browsers…

C-grade is the base level of support, providing core content and functionality. It is sometimes called core support. Delivered via nothing more than semantic HTML, the content and experience is highly accessible, unenhanced by decoration or advanced functionality, and forward and backward compatible. Layers of style and behavior are omitted.

In February of 2007, YUI’s browser grading chart looked like this:

Win 98 Win 2000 Win XP Mac 10.3.x Mac 10.4
IE 7.0 A-grade
IE 6.0 A-grade A-grade A-grade
Firefox 2.0.* A-grade A-grade A-grade A-grade A-grade
Firefox 1.5.* A-grade A-grade A-grade A-grade A-grade
Opera 9.* A-grade A-grade A-grade A-grade A-grade
Safari 2.0* A-grade

They had just dropped support for IE 5.5, Firefox 1.0, Netscape, and the Mozilla App Suite.

In 2010, they supported at A-grade IE 6, 7, 8 and 9, Safari 5, Firefox 3.6 and 4, Chrome (latest stable), and Android Webkit. C-grade browsers were listed as IE < 6, Safari < 3, Firefox < 3, Opera &lt 9.5, and Netscape &lt 8. They forecast that they would discontinue support for IE 6, dropping it to C-grade.

In 2011, they chickened out, and removed grades from their support page. You can find a list of dead links to their removed blog posts, if you want to grovel around in the internet archive and see what it used to look like. In fact, I wrote this post because all the source documents have been removed from the Internet.

Right as Yahoo was chickening out, jQuery Mobile was trying browser grading too. They had the guts to give out “F”s to Windows Mobile 6.1 and Blackberry 4.5 for a hot minute, if you want to grovel around in the Internet Archive.

They also gave “B”s:

B Medium Quality. Either a lower quality browser with high market share or a high quality browser with low market share.

They eventually settled on full support for A-grade browsers, support without AJAX page transitions for B-grade browsers, and plain but functional HTML for C-grade browsers.

Cool URLs don’t change… until your hosting provider says so

February 22nd, 2024

They say cool URLs don’t change, but a lot of the time you don’t get a choice. Google decided that http wasn’t good enough, so bluehost (my hosting provider for this blog) switched me over to https automatically. Then I got to have the fun mixed content warning on an older post that had an embedded image in it. At least they put in redirects.

Now my old university hosting has decided to close off AFS access to alumni. So what? Well, personal web pages are served from AFS, which means the small handful of things I have over there will start 404ing, without even the opportunity to put a 301 redirect on them. (I just noticed that they have changed the URL there too, but with redirects. So now there are two URLs that will go dead when they throw the switch.)

Sorry w3c. Cool URLs might not change, but my providers require me to be cringe.

(Not that anyone links to a basically dead blog anyway, but I was trying to be a good web citizen.)