Animate native lazy loading

Here’s another quick tip in my Today I Learned series.

Native lazy loading is awesome — just add loading="lazy" and you’re done. By default, images appear abruptly once they’re loaded. No transition; it’s just suddenly there. Depending on the file it could even load from top to bottom. My designers hate that. For a long time, I either relied on JavaScript libraries like Lozad.js or tried to explain why animations weren’t worth the overhead.

This never felt right — it’s just a simple animation! Turns out, the solution was quite easy. Not as easy as I’d like it to be, but more on that further down.

What we’re building

Here’s an example of two lazy-loaded images. The first one appears without an animation while the second one fades in. Reload the page to see the difference. Because the images are quite small (and my server is pretty fast), you probably have to throttle your connection to make it more noticable:

A monkey on a tree branch with a shocked expression, eyes wide, open mouth, looking directly at you
Default lazy loading
A monkey with a very relaxed expression taking a bath in a Japanese hot spring
Lazy loading with a fade transition

The HTML

Easily enough, we simply add the aforementioned loading attribute to our elements. Also, make sure to always specify image dimensions. Without them, lazy loaded images will cause layout shifts:

<img src="image.jpg" width="800" height="600" loading="lazy">

The CSS

We use a ⁠scripting: enabled media query to hide elements only when JavaScript is enabled. This targets our elements with an attribute selector, sets the initial style, and prepares the transition:

@media (scripting: enabled) {
  [loading="lazy"] {
    opacity: 0;
    transition: opacity 0.5s;
  }
}

The JavaScript

Here, we iterate over our elements using the same attribute selector and setup an animateIn function. The complete property tells us if an image has finished loading. We need this additional check because images that are already in the browser cache won’t trigger the load event. If we relied on the event listener alone, some images would stay invisible forever 😱

document.querySelectorAll('[loading="lazy"]').forEach(element => {
  const animateIn = () => {
    element.style.opacity = 1;
  };

  if (element.complete) {
    animateIn();
  } else {
    element.addEventListener('load', animateIn);
  }
});

A note by Amadeus Maximilian on Mastodon: if you’re using onload attributes instead of an external script, you can skip the complete check. Cached images will immediately fire the load event.

Why not CSS only?

While researching, I stumbled over this 14 years old Gist talking about a :loaded pseudo class. It’s unfortunately still not a thing so we have to resort to JavaScript for detecting the loaded state. I also expected @starting-style to work, but it doesn’t take into account the loading state.

More complex animations

Since we have to use JavaScript anyways, you could also handle the animation there with libraries like GSAP or Motion. Just make sure you don’t go overboard with the animation and check for prefers-reduced-motion when your animation involves movement. I’d also suggest avoiding lengthy animations that could make your site feel slow.