Building a Lazy Loader from Scratch in React (Part 3)

Stay updated with our latest articles:


Finally, it's time for the third and last installment of this series of how to build a lazy loader from scratch using React and the IntersectionObserver API.

Here's a quick recap of the last two installments:

In the first part, we saw what exactly is a lazy loader; how to bring React and the IntersectionObserver API together in powering a very simple, minimal lazy loader; and finally implemented the lazy loader using a multitude of ideas from React and the observer API

Then in the , we solved a couple of issues with this minimal lazy loader, such as that of cumulative layout shift (CLS) and not being responsive (i.e. the image was overflowing the viewport on smaller devices). In the end, we obtained a fully-functional, responsive and free-from-CLS-issues lazy loader, after quite a long fruitful discussion.

Now, in this last part, we shall work on building perhaps the coolest of all features — an elegant, fade-in effect to introduce our lazy image to the world.

Ok, well that got way too dramatic!

In this chapter, we'll see how to implement a fade-in transition as a lazy image loads into view. We'll be using knowledge from CSS, in particular from the topics of CSS transitions, opacity, and some other ad-hoc properties.

Without any further ado, let's get started.

Understanding the effect

Before we begin, let's spare some time to understand exactly what we're trying to achieve over here.

Go on, open the link below, and try to exhibit the best of your analysis skills to infer what is happening as the lazy image on the page loads into view.

Live Example

Supposing your analysis is complete and that you're now ready to cross-check it, let's together see what's happening in the page above.

As soon as the image successfully loads, it doesn't snap into view like what we've been seeing thus far in this series. Instead, the image smoothly appears into view.

Moreover, another thing worth noting in the example above is that while the image is still in the process of loading, there is an animation going on in the background of the container for...

What do you think, why is the animation happening?

Yes, you guessed it right! The animation is happening in order to signal the loading of the underlying image.

So in deconstructing the given example, we see that there are two things that we ought to accomplish in the following discussion:

  • One is to implement the fade-in transition itself.
  • The other is to implement some kind of a loading indicator (it could be an animating background, a gif, an HTML-powered element, anything).

Alright, with this high-level overview of the effect and its prerequisites, it's time to get into it and start building it. We'll start with the loading indicator.

The loading indicator

While a lazy image loads, after its scrolled into the viewport, we must use something to clearly indicate to the user that the image is actually being loaded and that the application is not stuck at a blank view.

It's much better to show some progress in action rather than showing nothing and assuming that the user will be able to infer on his/her own that something is about to show in the blank gray container.

No, don't expect your user to infer things.

You have to tell them about it, in this case, tell them that the image is loading in the background. This leads to a better UX (user experience).

Now for the loading indicator, we have a handful of choices:

  • Use a generic loading gif, like the spinner icon.
  • Use a branded gif that aligns with our brand icon.
  • Use an HTML element along with some CSS.
  • Use some kind of CSS ingenuity in the background.

We'll be choosing the last one because we feel that it's really easy to implement it and also it arguably gives the most modern vibe these days in React apps (in fact, in all kinds of apps).

You're obviously free to choose anything you like.

The ingenuity we're talking about here is a shimmer effect. We've already covered how to create a shimmer loading animation in the following article:

How to create a shimmer effect using HTML and CSS?

Go through the article and see how to create a very popular effect in today's modern application world. It's your time to shine through. (Or should we say, shimmer through?)

Here's our CSS code for .lazy, since it's the element with the background:

.lazy {
   position: relative;
   background: linear-gradient(-45deg, #f3f3f3 40%, #fafafa 50%, #f3f3f3 60%);
   background-position-x: -50%;
   background-size: 300%;
   animation: shimmer 1.5s linear infinite;

@keyframes shimmer {
   to {
      background-position-x: -350%;

Obviously, we can only see .lazy when .lazy_img is hidden. So, let's go on and hide .lazy_img using display: none (we'll be changing this shortly below):

.lazy_img {
   position: absolute;
   top: 0;
   left: 0;
   width: 100%;
   height: 100%;
display: none; }

And here's our result:

Live Example

Obviously, there's a lot to rectify in here, for the implementation is far from perfect. And that's what we'll be doing in the next section.

Added a loaded state

Notice the animation created in the previous section. Yes it's great, elegant, amazing, spectacular, and whatnot, but all this sounds good only as long as we're giving a concern to efficiency.

When the lazy image loads into view, supposing that we display the image in .lazy (by removing display: none), there's absolutely no point in keeping the animation running; the background gets hidden and so we should also stop the animation then and there.

And for this, we obviously need an HTML class that could be used in the CSS to contain the styles when the lazy image gets loaded successfully.

Let's call it .lazy--loaded (following the same slightly modified BEM naming convention that we've used in the preceding chapters).

But where will this class come from? Well, it'll come from our React app, exactly when the lazy image gets loaded.

In the following code, we extend our LazyImage component with another state value, called loaded, to represent whether the underlying <img> has successfully loaded or not.

function LazyImage({ src, width, height }) {
   const [srcExists, setSrcExists] = useState(false);
const [loaded, setLoaded] = useState(false); const divRef = useRef(); useEffect(() => { divRef.current.setSrcExists = setSrcExists;; }, []);
function onLoad() {
} return ( <div style={{ maxWidth: width }}> <div style={{ paddingBottom: `${height / width * 100}%` }} ref={divRef} className={'lazy' + (loaded ? ' lazy--loaded' : '')} >
<img onLoad={onLoad} className="lazy_img" src={srcExists ? src : null} /> </div> </div> ); }

Initially, loaded is false and rightly so — the image doesn't get loaded to begin with. loaded is transitioned to true inside the onLoad() event handler of the <img> element, which fires when the image successfully loads.

Observe the application of the .lazy--loaded class in the code, in line 20. Only when loaded is true is this class added to the .lazy element.

With the class being created at the right time, let's turn to the CSS and remove the animation style in .lazy--loaded.

.lazy--loaded {
   animation: none;


We're now just left with one last thing and that is implementing the fade-in effect on the <img>. Fortunately, using precisely three properties, it's really quick to do so.

Let's move on.

Implementing the effect

To implement any kind of a fade effect on any element, we need essentially three things, or better to say, three CSS properties: opacity, visibility, and transition.

Let's see what each of these does.

  • opacity literally controls the opacity, i.e. the fade amount, of the element. A completely faded-in element would have an opacity of 1 (fully opaque) while a completely faded-out element would have an opacity of 0 (completely transparent).
  • visibility controls the visibility of the element. This is important so that when the element is completely faded-out, it's not there on the webpage. (opacity: 0 just makes the element transparent, not actually hidden from active view, thus, we can interact with everything inside the element)
  • transition is the thing that allows us to 'smoothly' transition from a faded-in state to a faded-out state, or vice versa.

And that's pretty much it.

In our lazy loader, all what we need to do now has to be done inside the CSS; we're free from our JSX code.

The good, old CSS.

First things first, we'll make the <img> initially completely faded-out along with the application of transition, and also remove the previous application of display: none:

.lazy_img {
   position: absolute;
   top: 0;
   left: 0;
   width: 100%;
   height: 100%;
visibility: hidden; opacity: 0; transition: 0.6s ease-out;

This faded-out effect needs to be removed (thus giving a fade-in effect) exactly when the <img> successfully loads. And that's when the .lazy--loaded class is set on the .lazy element.

This leads us to the following code:

.lazy--loaded .lazy_img {
   visibility: visible;
   opacity: 1;

opacity is set to 1 while visibility is set to visible when the <img> has been loaded. Simple.

And that completes our implementation of the fade-in effect. Time to test it now.

The final lazy loader

Recall the following code in App.jsx that we use to test our lazy images:

import LazyImage from './LazyImage';

function App() {
   return (
         <h1>Demonstrating lazy loading (with a fade-in effect)</h1>
         <p style={{ marginBottom: 1000 }}>Slowly scroll down until you bring the lazy image into view.</p>
         <LazyImage width={640} height={424} src="/image.jpg" />

export default App;

Everything is the same as left in the previous part of this series, except for the heading. We don't need to add/remove anything since nothing has changed from the perspective of the consumption of LazyImage.

Here's the link to the page:

Live Example

Go on and scroll down and wait for 2 seconds before you finally see the lazy image putting aside its laziness.

And that's essentially it.

Congratulations! We've completed our series of implementing a lazy loader from scratch.

Just one last thing before we conclude.

In all the examples we've seen thus far, we've been using setTimeout() to manually delay the time until an image is requested for and ultimately loaded. Now that we've completely tested everything in the lazy loader, it's the high time to let go of this setTimeout() call.

In the code below, we do so:

function ioCallback(entries, io) {
   entries.forEach(entry => {
      if (entry.intersectionRatio >= 0 && entry.isIntersecting) {
   setTimeout(() => {
   }, 2000);


Never miss an article

Follow Codeguage on either of the following channels and stay updated with every latest blog article that we publish.

Improve your web development knowledge, today!