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:
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 thecomplete
check. Cached images will immediately fire theload
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.