A Guide to JavaScript's Intersection Observer API

A Guide to JavaScript's Intersection Observer API

Reveal content on scroll with the JavaScript Intersection Observer API

·

11 min read

In JavaScript, tracking the visibility and intersection of an element on a webpage relative to another element has always been challenging. Traditional methods used to monitor the visibility of elements on a page become resource-intensive when a webpage’s content becomes heavy. Which slows down the computer and makes web applications run slowly. With user satisfaction being a top priority, the need to create faster web applications has become increasingly important.

On this note comes the JavaScript Intersection Observer API. The Intersection Observer API makes it easy to track the position of an element on a webpage relative to another element without demanding more of a computer's processing power.

If you've ever noticed content slide in and out while you scroll through a webpage, one way to implement this feature is with the JavaScript Intersection Observer API. The Intersection Observer API does this by keeping track of when elements relative to each other intersect on a page and then triggering an action. This is one of the many uses of the Intersection Observer API.

In this article, you will learn what the intersection observer API is, understand its inner workings, and how to use it to reveal content on scroll.

Prerequisite

To follow along with this tutorial, I expect you to have a basic knowledge of HTML, CSS and a basic knowledge of JavaScript.

What is the Intersection Observer API?

In this tutorial, we will refer to the Interaction Observer API as IOA from now on for convenience. IOA, as the name suggests, is a JavaScript web API that watches for when two elements on a webpage intersect with each other and perform an action. These elements are referred to as the target (child or children) and root (ancestor) elements.

What are the target and root elements? Think of the root element as a container (ancestor) and the target element as the child.

To create a mental picture of this, imagine a markup of a section element wrapping a paragraph (<p>) and a div element. In this context, the <section> element would be the root element, while the <div> and <p> nested within it would be the target (children) elements.

<section>
  <p></p>
  <div></div>
</section>

You now understand what IOA is. Let's proceed by setting up our HTML markup.

HTML/CSS layout.

The markup will contain three section elements with the following content:

  • The first section element containing an h2 and a paragraph element.

  • The second section element containing a div element with a nested image.

  • The third section element containing an h2 tag.

The second section element is what we will be working with. Extra sections were only added so we would have room to scroll the page.

<body>
  <section class="section-1">
    <h2>Section One</h2>
    <p>Scroll down...</p>
  </section>
  <br />

  <section class="section-2">
    <div class="image">
      <img src="/img/joshua-reddekopp-YQnTaPvCLGo-unsplash.jpg" alt="" />
    </div>
  </section>
  <br />

  <section class="section-3">
    <h2>Section Three</h2>
  </section>
</body>

That is all for the HTML. For the CSS,

  • Select the section elements on the page and give them a height of 100vh, and center their content using flex.

  • Resize the image to a width of 400 pixels (not compulsory) and set a border radius of 10px on it.

  • Select the section elements with their unique class names and set a different background color for each section for distinction.

  • Create an independent class and name it hidden-left (call it whatever you like), that contains an opacity property with a value of 0. And then create a transform property that translates 100% horizontally to the left. The class will be applied when the root and target elements intersect.

  • And finally, select the div with the class name of image that is within the second section element, add a transition property all for 1.5 seconds and a timing function of ease-in.

    As shown below

section {
    height: 100vh;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
}

img {
    width: 400px; 
    border-radius: 10px;
}

.section-1 {
    background-color: #dbe6eb;
}

.section-2 {
    background-color: skyblue;
}

.section-3 {
    background-color: rgba(255, 0, 0, 0.784);
}

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

.image {
    transition: all 1.5s ease-in;
}

You should now have the below layout 👇

Now let's delve into the observer.

Creating an Intersection Observer.

To start the IOA, you need to create its instance with the new operator and pass it a callback function and an options object (more on those in a minute) as arguments, and store its instance in a variable of your choice. Consider the code below:

const observer = new IntersectionObserver(callBackFunction, options);

That is how you create an IOA. However, the observer isn't functional yet because we passed it an empty callback function and an options object. With that said, let's dive into the callback and options object.

The callback function.

A callback function is a function passed as an argument to another function. In this case, we passed the callback function to the IOA, which will be called when a target element intersects with a root element.

The callback function is passed an entries object as a parameter (callbackFunction(entries)), which holds information about the element being targeted or observed. This includes methods like isIntersecting(), isVisible() and target(). Additionally, the entries object can also be treated like an array-like object, allowing the use of array methods to extract information from it. Further details on isIntersecting(), isVisible() and target() will be discussed later in the tutorial.

Now, let’s talk about the options object.

The options object.

The options object is an object passed to the intersection observer as a parameter. It controls when the callback function is triggered and contains the following properties:

  • root

  • rootMargin

  • threshold

const options = {
    root: 
    threshold: 
    rootMargin:
}
  • root: Recall that the intersection observer watches for when a target element intersects with a root element? The root property in the options object is where you specify the root element you want the target element to intersect with. It takes null as a default value, which represents the browser viewport. Unless you explicitly change it, it stays that way.

    The browser views the root element as a rectangle-bounding box surrounding the target element having four sides (left, right, top, and bottom). Even if you specify a root element that isn't rectangular, the browser will automatically create a rectangular bounding box around it in memory.

  • threshold: As the observer watches for when the root and target intersect, you can decide not to trigger the observer immediately and wait till a portion of the target element is visible before the observer is triggered. So in summary, threshold controls the percentage of the target element that needs to be visible before the observer is triggered.

    It takes numeric values between 0 and 1. 0 standing for 0% and 1 standing for 100%. When you specify 0, the callback function will be called immediately or even when 1 pixel of the target element is visible on the page while 1 would wait till 100% of the target shows in the viewport before the callback function is triggered. And as you must have guessed, 0.2 is 20%, 0.5 is 50%, and so on.

  • rootMargin: Recall that the root element is a rectangular bounding box? Meaning it has four sides. The rootMargin is used to either shrink or extend the margin around the root element. It accepts values in pixels like the margin property in CSS (50px) or percentage values (50%). The only difference is that you store the values as string data. rootMargin generally controls the way intersection is calculated on the page. For example:

    If you specify a rootMargin: "50px", 50 pixels will be added to the top, bottom, left, and right dimensions of the root element. A positive value will extend the intersection time, while a negative value will delay it. For example, if the observer was to be triggered when 50% of the target element enters the viewport, specifying a rootMargin: “50px” would extend it a little longer.

    Consequently, the observer will no longer wait for the initial 50% but, will execute the callback function while the target element is still 50 pixels away from the root. In contrast, if you specify a negative value of (”-50px”), the observer will wait for the initial 50% specified plus an extra 50 pixels of the target element to come into view before it triggers.

Note that the most important property of the IOA is threshold . You can exclude the other properties and the observer will still run, but you can't do without threshold.

You've heard so much, let's get our hands dirty.

Implementing the intersection observer

This is the point where the DOM(Document object model) elements come into view.

Looking at the markup, let's select the element we want to the IOA to observe from the DOM and store it in a variable.

  • Select the section element with the class of section-2.

      const sectionElement = document.querySelector('.section-2')
    

Next, let's select the image that will slide in and out of the page.

  • Select the div element with the class of .image

      const image = document.querySelector('.image')
    

Next, let's create and store the observer containing a callback function and an options object in a variable sectionObserver Like so:

const sectionObserver = new IntersectionObserver(callBackFunction, options)

Next, we will provide our callback function and our custom options object:

For this observer, we want the viewport to be our root element, thus, the root element will hold a value of null (which is the same as not providing it at all). We also want the observer to kick in when 30% of the target element comes into the viewport, thus the threshold will hold a value of 0.3. Finally, we will wait till another 50 pixels of the target element come into the viewport before the observer is triggered. Thus, the rootMargin will hold a value of '-50px'.

const options = {
    root: null,
    threshold: 0.3,
    rootMargin: '-50px',
}

That is all about the options object. Let's provide the callback function now.

function callBackFunction(entries) {
    const [entry] = entries
    if (entry.isIntersecting) {
        image.classList.remove('hidden-left')
    } else {
        image.classList.add('hidden-left')
    }
}

You might be confused now so, let me run you through what each line of code is doing.

  • callbackFunction(entries), the callback function takes an entries argument as a parameter. That's why you see an entries parameter there.

  • const [entry] = entries: remember I said the entries object is an actual JavaScript object and represents the target element. So, to access the information within the entries object, we use array destructuring to get information about that target element. It contains information such as:

    1. isIntersecting(): returns a boolean value indicating whether the element being observed is interacting with the root element or not.

    2. isVisible(): checks if the element is visible or not. To get a comprehensive list of the methods available in the entries object, use console.log(entries) within the callback function.

    3. target() : represent the element that is been observed.

  • if (entry.isIntersecting): This line checks if the target element is interacting with the root element or not using the isIntersecting() method. If it is, the .hidden-left class is removed from the image. If otherwise, the .hidden-left class is added.

That will be it for the callback function. However, the observer still wouldn't kick in at this point because we haven't given it the green light. Before then, note that the above methods (isIntersecting(), isvisible()) are specific to the entries object alone. The observer itself (sectionObserver) provides you with a list of methods specific to itself. To see them, use console.log(sectionObserver).

Notice that there are a couple of methods available to the observer here. We won’t be using all, but just the observe() and unobserve() methods.

As you might have guessed, the observe() method watches for when a target element intersects with a root element, while the unobserve() method stops the observer from watching a target element when certain conditions are met.

With that out of the way, we can now use the observe() method to observe the section element like so:

sectionObserver.observe(sectionElement)

Now you should have behavior like the demo below. 👇

First Demo

Notice how the observer triggers when you scroll up or down the page. There is a way to trigger the observer once per page reload using the unobserve() method.

Unobserve method

With the unobserve method, you can set the IOA to observe an element just once per page reload. To initiate this behavior, we will pass the observer as a parameter to the callback function and call the unoberve() method within the callback function to stop observing that specific element on the page. Consider the code below.

function callBackFunction(entries, observer) {
    const [entry] = entries
    if (entry.isIntersecting) {
        image.classList.remove('hidden-left')
        observer.unobserve(entry.target)
    } else {
        image.classList.add('hidden-left')
    }
}

Let's examine the piece of code we added: observer.unobserve(entry.target). Looking at this code, you would notice that entries isn't the target, instead, we made entry the target. This is so because entries consists of an array of entries. That is the total number of the elements observed. Hence, the unobserve() method expects a target element, not an array of entries. That is why entry which represents the last element that was passed was made the target.

And just if you're wondering why we had to pass the observer as a parameter to the callback function, well, we only did that so we could access the unobserve() method. Because the unobserve() method is specific to the IOA, not the callback function.

This code now checks for when the root element intersects with the target element, and if so, removes the hidden-left class from the image and calls the unobserve() method afterward. This will now trigger the observer once per page reload. As evident in the demo below.

Second Demo

Conclusion

And there you have it, the JavaScript Intersection observer API. It's been an interesting ride I know. However, this article is by no means complete. For the sake of length, I couldn't get to discuss cases where you would want to observe multiple elements on a page. There is a slightly different approach to that, which I would like to talk about in the next article. Also, there are various ways to implement the Intersection Observer. So, see this as one of many ways to use the observer. I hope to catch you on the next one.