Archive for the ‘Uncategorized’ Category

Incompetent Browsers: Building the Fallback Experience

Monday, 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

Wednesday, 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

Tuesday, 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

Monday, 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

Saturday, 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


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

Thursday, 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.)

Pillar Technology

Monday, February 8th, 2010

So, my search is over, as I’ve accepted a position with Pillar Technology. I’ll be doing agile development, agile coaching, and writing code.

Seeking Employment, Again

Tuesday, January 12th, 2010

Well, good things never last, do they?

We had a layoff at my work; I was one of the IT people who got cut. The usual reasons… Money is tight, etc.

If anyone knows of an opening for a programmer in western Michigan, please let me know. I’ve linked my resume on the sidebar.

Initial Post

Tuesday, March 25th, 2008

This site is for discussion of bacon driven coding and other
thoughts on improving software development.