Revealing Contents on Scroll Using JavaScript’s Intersection Observer API

Revealing Contents on Scroll Using JavaScript’s Intersection Observer API

A Beginner's Guide to The Intersection Observer API.

Featured on Hashnode

Have you ever visited a website where as you scroll down the page, the contents of the webpage gradually begin to reveal themselves as you approach them? You got to admit, it's a pretty sleek feature. Have you ever wondered how you could implement this feature in your projects without using third-party libraries or plugins? Well, JavaScript has a native Intersection Observer API that lets you do just that... and much, much more. In this article, we will discuss how this Intersection Observer API works and how we can use it to detect the visibility of an element by building a simple web page that implements this "reveal contents on scroll" feature.

Prerequisites

  • A basic knowledge of JavaScript (beginner level is acceptable as I'll explain everything in great details like I was explaining to a 5-year-old. :)
  • A basic knowledge of HTML and CSS (you've built at least one basic webpage with them).
  • A code editor (VS Code recommended).
  • And a browser of course (Chrome or Firefox recommended).

What is the Intersection Observer?

The Intersection Observer API is simply a new way to observe (monitor) the position and visibility of an element in the DOM relative to another root element and to run a callback function if these elements intersect (meet).

Now you might wonder, What exactly is a root element? Well, a root element is simply an element that is a parent or container element to other elements. Meaning, If we created a div in an HTML document and inside this div we placed a p text, the div becomes the direct root element (parent) of the p text as it is what contains the paragraph.

<body>
    <div>
      <p>Lorem, ipsum.</p>
    </div>
</body>

Based on this logic, we can safely say the body is also the immediate parent to this div and also a grandparent to the p text. But you know what else is the ancestral root element of everything in the DOM? The browser viewing the HTML document becomes a container (root) to whatever area of the webpage that is visible to the browser's viewport (screen) at any time.

So in essence, the Intersection Observer API can be used to observe an element to see if that element intersects (meets or passes across) its root element in the DOM or if it simply enters or leaves the browser's viewport. And for the observer to trigger a callback function when this event takes place.

Note: A callback function is simply a normal function that is provided to another function as that function's argument (the actual value for its parameter).

Below is an image I've prepared that illustrates an actual Intersection in action, It should give you an idea of how it works, but if it's still unclear, don't sweat it... I'll explain everything in a minute. Group 12x.png

Creating a Basic HTML/CSS Page

Now that we know what an Intersection Observer is, let’s dive into its implementation. We’ll start by creating a simple HTML page with 3 sections, the first and third section is of little interest to us as we will mostly be working with the second section, we simply want more room to be able to scroll down the page.

  <body>
    <section class="section-1">
      <h2>Section 1</h2>
    </section>
    <section class="section-2">
      <img class="img" src="background.jpg" alt="" />
    </section>
    <section class="section-3">
      <h2>Section 3</h2>
    </section>
  </body>

Now for the CSS, we'll give each section a height of 100vh, center the contents of each section using flex, then give the image a fixed responsive width and make each section obvious by applying a background colour to separate them. Lastly, we will create a hidden class that will be responsible for hiding and revealing our content later on using JavaScript.

h2 {
  font-size: 3rem;
}

.img {
  width: 95%;
  max-width: 600px;
  transition: all 1.5s ease-in;
}

section {
  background-color: #dbe6eb;
  height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  overflow: hidden;
}

.section-2 {
  background-color: #fff;
}

.hidden {
  opacity: 0;
  transform: translateX(100%);
}

The resulting layout👇 Layout Output.gif Great, we have our basic webpage set up, now let's dive into JavaScript and talk about the Intersection Observer.

Implementing the Intersection Observer API

To use the intersection observer API we need to start by first creating one using its default object constructor function.

new IntersectionObserver();

This constructor function is basically an in-built function in JavaScript that is responsible for creating an observer that we can then use to observe our target element, and this constructor has two parameters that take in two arguments.

The first argument is a callback function that is called when there is an intersection with the observed element. Remember what a callback function is? Simply a normal function that is passed to another function as that functions argument, so basically the Intersection Observer is given a function to call when there is an intersection.

The second argument is an object containing options to customize the observer. This argument is actually optional and can be left out, if not provided the observer will use its default options (more on that later). Now let's create an Intersection Observer.

Firstly, let’s select the target element we want to observe.

const section = document.querySelector(‘.section-2’);

Then let’s create an observer to observe this section element

Const theObserver = IntersectionObserver(callbackFunction, options);

Once we have created an observer, we have to tell the observer what target element to observe using its built-in observe() method on the observer. This method receives the target element to be observed as its argument. So let's do just that.

theObserver.observe(section);

Let’s go through what we just did now, we first selected a target element to be observed section, then created an observer theObserver, and finally we told the observer what to observe by passing the target element to be observed into the observer using the observe() method. That's it, we have everything set up, the only problem is we have neither defined our callbackFunction nor the options object so they are currently undefined.

The Options Object

Now, let's define the options that we initially passed into the observer constructor on creation as it is still linked to nothing at the moment. I’ll start with defining the options object (recall that this is used to customise the observer) and then explain each property inside.

Note: Because an object can not be hoisted (used before it is defined), to avoid errors it should always be defined at the top before passing it to the observer, or the actual object itself can be passed as an argument to the Observer when creating the observer.

With that in mind, let's rewrite the JavaScript code we have written so far in the appropriate order.

const section = document.querySelector(‘.section-2’);

const options = {
  root: null,
  threshold: 0.3,
  rootMargin: "-100px",
}

const theObserver = new IntersectionObserver(callbackFunction, options);
}
theObserver.observe(section);

root: This is where we specify what exact root element we want our observed element to intersect against. The root is usually an ancestor of the target element in the DOM (i.e a container or parent element of the observed element). The value is set to null if we want the observed element to intersect with the entire browser's viewport (that's the default value). Think of the root element as a rectangular “capturing frame” that the observed target element needs to make contact with.

threshold: The threshold is basically the percentage of the observed target that should come into view before it can be considered an intersection. Confused? Well, do you want the target element to completely come into view (become 100% visible) before triggering the callback? or do you want just a fraction of it to be visible in the browser's viewport before running the callback? That's what you have to specify as the threshold.

  • The threshold receives a numeric value between 0 and 1 which represents the percentage in which the target intersects the root. Meaning 0.1 represents 10%, 0.2 is 20%, 0.5 is 50%, and 1 is 100%. The default value is 0, which means the intersection occurs as soon as the observed element hits even 0px of the root (about to come into view).

  • The received value can either be a single value (which means you want the target element to make a single intersection) or multiple values in an array (which means you want the target element to make multiple intersections and to run the callback for each intersection).

  • The intersection is triggered each time the target element is making an entry or exiting the root (viewport). Meaning if the threshold is 0.1, the intersection will occur when 10% of the element is visible and another 10% intersection will occur when it is leaving the viewport.

rootMargin: Because the root element is considered to be a rectangular frame (bounding box) with four sides, margins (positive or negative) can be applied to the root element just like in CSS, to grow or shrink its frame of intersection.

Recall that the browser's viewport is our root element (which is a rectangular frame) and we set the threshold to 0.3? that means the intersection should occur when 30% of the observed element comes into view. Now, we also went ahead to set the rootMargin to -100px, this will shrink the intersection frame by -100px and the intersection will no longer occur at our specified 30% threshold but would instead wait until another 100px of the target element has come into view after the initial 30% (think of it as adding 100px to the 30%).

If the margin was set to 100px the intersection would be triggered while the observed element was still 100px away from the 30% threshold (the negative margin shrinks the frame of intersection while the positive margin grows it/pushes it outwards).

The Callback Function

We can now define our callback function, which is the last piece of the puzzle. So let's define the function but we won't do anything with it just yet because we have to first take a look at the behaviour of the Intersection Observer and how it actually works.

When a webpage with an Intersection Observer is initially loaded for the first time, the Observer always fires the provided callback function once by default regardless of an actual intersection or not (I know, it's a weird behaviour). When this occurs, the observer passes an entries array to the callback function, and this entries array itself contains an IntersectionObserverEntryobject inside of it. This object contains several properties that describe the intersection between the target element and its root container.

Enough talk... let's define the callback function so we can see the object itself.

function callbackFunction(entries) {
  console.log(entries);
}

We have defined the callback and provided an entries parameter for the observer to pass its observations and we are logging to the console the actual argument that is passed into the parameter when the callback is fired. If we now load the site and open up the dev tool, below is what we see 👇

observer-entries.png

As you can see in the dev tool the entries contain several details about the intersection, you can explore each of these properties on your own but in this article, we'll only be looking at the following:

  • target: This is the actual element that is being observed by the observer for an intersection with the root element.

  • isIntersecting: This returns a Boolean value of true if the target element being observed is currently intersecting (if the threshold of the target element has intersected) with the root element or false if that's not the case.

  • isVisible: This returns a Boolean value of true or false which indicates whether or not the target element being observed is currently visible in the browser's viewport.

Now that we understand what returned values these properties contain, we can now write a proper function that checks the entries object to see if our target element has intersected with the browser's viewport and to do something with that info.

But before proceeding to the callback, let's select the content we wish to reveal on Intersection.

const imgContent = document.querySelector(".img");

Now let's define the callback, before going through it line by line.

function callBackFunction(entries) {
  const [entry] = entries;
  if (entry.isIntersecting) {
    imgContent.classList.remove("hidden");
  } else {
    imgContent.classList.add("hidden");
  }
}

Now let's dissect the function line by line.

  • const [entry] = entries:
    Recall that the Observer passes an entries array to the callback containing an IntersectionObserverEntry object? I simply deconstructed (extracted the object in) the array and stored it in an entry variable to make it easier to directly access the properties of interest stored in that object.

  • if (entry.isIntersecting) { imgContent.classList.remove("hidden") }:
    Afterwards, we check the isIntersecting property to see if our target element (the target section-2) has intersected with the viewport, If the value is true we remove the hidden class that we initially created in our CSS from the image to reveal it (you might be wondering why we are removing a hidden class that we never did add to the image... the else block below is your answer).

  • else { imgContent.classList.add("hidden") }:
    Else if the isIntersectingvalue is false we add the hidden class to the image, But do you recall that the callback function gets fired once by the Observer when we load the webpage? When this happens the initial entry is passed to our function. Since there is no Intersection, this else block will run, thereby hiding our image on load.

That's all, our webpage should now behave as expected final webpage.gif

Excuse the lag in the recording, my screen recorder was acting up. But as you can see, as we scroll towards the observed section, once 30% of the element comes into view we ought to get an Intersection, but because we set the rootMargin to -100px the Intersection will now occur when the target section scrolls another 100px into view, then an Intersection is triggered and the callback is fired. The image then gets revealed and slides back into its original position as the hidden class gets removed.

And as the observed section scrolls out of view (exits) the callback is fired again by the Observer, If you can recall, we discussed how the observer is fired on entry and again fired when exiting the viewport... and since the second time there is no actual intersection, the hidden class is added again and the image is hidden as we scroll out of view.

Here is the entire JavaScript code we wrote.

const section = document.querySelector(".section-2");
const imgContent = document.querySelector(".img");

const objOptions = {
  root: null,
  threshold: 0.3,
  rootMargin: "-100px",
};

const sectionObserver = new IntersectionObserver(callBackFunction, objOptions);
sectionObserver.observe(section);

function callBackFunction(entries) {
  const [entry] = entries;
  console.log(entry);
  if (entry.isIntersecting) {
    imgContent.classList.remove("hidden");
  } else {
    imgContent.classList.add("hidden");
  }
}

Conclusion

Congrats!!! You have successfully implemented a basic Intersection, but there are a few things I didn't get to cover as the article was getting too long. We did not cover how to observe multiple elements, nor did we discuss how to unobserve an element after Intersection. For this, I've made part two of this article where we cover these and also build another webpage where we observe multiple sections and do something for each of their respective Intersection. Below is the part 2.

And if you were wondering what else you could do with the IntersectionObserver, your imagination is the limit, you could implement an Infinite scrolling page, a lazy loading feature, a sticky menu and so much more. I'll probably make more tutorials covering these features so stay tuned.

Support Me☕🤝

If you liked my article and found it helpful, you can buy me a coffee using any of the links below.👇

Also, do well to like and follow for more contents, and if you've got any questions or spotted any errors... please do well to leave some feedback as this is my first technical article.

References