Accessible lazy-loading with a <noscript> fallback

Designing for the probably even less than 0.2% may sound like an overkill. Static sites (such as Gatsby, Svelte, Jekyll, Hugo, 11ty etc.) will cover non-script users most of the time. But if you need more fine-grained control, <noscript> is here to save the day.

If you turn off JavaScript in a browser, and open up any React app, you’re quite likely to see the sentence “This app works best with JavaScript enabled” or similar. Looking at the source code1, this is the content of a <noscript> tag — content that is only rendered when JavaScript is not available.

You can put mostly anything inside <noscript>, including <meta>, <link> and <style> tags. It’s a good place for fallback content and stylesheets.

A real-life use case: lazy-loading images

Using a library like vanilla-lazyload already improves accessibility of a website, because it makes load time much faster, hence a smoother experience for users on average networks or devices.2

The core concept behind lazy-loading is either omitting the src and replacing it with a data attribute, or using a tiny version of the image with a blur-up effect.3 When the actual asset is loaded, it’s faded in using CSS. I’m omitting image width and height here, but you should try to set it to prevent content reflow.

HTML

<img data-src="cat.png" alt="My cat" class="lazy" id="cat" />

JS

const catImg = document.getElementById(‘cat’)
const fullsizeSrc = catImg.dataset.src // from the data-src attribute
const loader = new Image()
loader.onload = () => {
	catImg.src = fullsizeSrc
  catImg.classList.add(‘loaded’)
}
// VERY important: .onload() has to come before .src assignment
loader.src = fullsizeSrc

CSS

.lazy {
  opacity: 0;
  transition: opacity .5s ease;
}

.lazy.loaded {
	opacity: 1;
}

The (admittedly edge-case) problem with these is, if there’s no JavaScript, the images will never be loaded. The solution is to add a non-lazy version for every lazy image, like so (and you can do this programmatically, at build time):

  <img data-src="cat.png" alt="My cat" class="lazy" id="cat" />
+ <noscript>
+  <img src="cat.png" alt="My cat" />
+ </noscript>

It will still be super fast, because whatever is inside <noscript> won’t load unless JavaScript is off. There’s still one issue to fix, though: hiding the lazy image placeholders when the <noscript> version kicks in. Here’s how, anywhere in your <head>:

+ <noscript>
+   <style type="text/css">
+     .lazy { display: none; }
+   </style>
+ </noscript>

Alternatively, you can load an external stylesheet too.

That’s it for inclusive lazy-loading — let me know if I’ve missed something!


Notes


  1. See react-redux-ts/index.html at master · c0derabbit/react-redux-ts · GitHub

  2. Let’s not forget that as developers and/or designers, both our devices and network are likely to be superior to that of the average user.

  3. I’ve written about a React solution for the blur-up lazy loading here.