JavaScript Intersection Observer: Everything You Need to Know

The Intersection Observer API is used to asynchronously observe changes in the intersection of an element with the browser’s viewport. It makes it easy to perform tasks that involve detecting the visibility of an element, or the relative visibility of two elements in relation to each other, without making the site sluggish and diminishing the user experience. We’re going to learn all about it in this article.

Uses of Intersection Observer

Before we start exploring the Intersection Observer API, let’s look at some common reasons to use it in our web apps:

Infinite scrolling

This is a web design technique where content is loaded continuously as the user scrolls down. It eliminates the need for pagination and can improve user dwell time.

Lazy loading

Lazy loading is a design pattern in which images or other content are loaded only when they are scrolled into the view of the user, to increase performance and save network resources.

Scroll-based animations

This means animating elements as the user scrolls up or down the page. Sometimes the animation plays completely once a certain scroll position is reached. Other times, the time in the animation changes as the scroll position changes.

Ad revenue calculation

We can use Intersection Observer to detect when an ad is visible to the user and record an impression, which affects ad revenue.

Creating an Intersection Observer

Let’s take a look at a simple use of an Intersection Observer in JavaScript.

index.js

const observer = new IntersectionObserver((entries) => {
  for (const entry of entries) {
    const intersecting = entry.isIntersecting;
    entry.target.style.backgroundColor = intersecting ? 'blue' : 'orange';
  }
});

const box = document.getElementById('box');

observer.observe(box);

The callback function receives an array containing objects of the IntersectionObserverEntry interface. This object contains intersection-related information about an element currently being watched by the Observer.

The callback is called whenever the target element intersects with the viewport. It is also called the first time the Observer is asked to watch the element.

We use the for...of loop to iterate through the entries passed to the callback. We’re observing only one element, so the entries array will contain just the IntersectionObserverEntry element that represents the box, and the for...of loop will have only one iteration.

The isIntersecting property of an IntersectionObserverEntry element returns a boolean value that indicates whether the element is intersecting with the viewport.

When isIntersection is true, it means that the element is transitioning from non-intersecting to intersecting. But when it is false, it indicates that the element is transitioning from intersecting to non-intersecting.

So we use the isIntersection property to set the color of the element to blue as it enters the viewport, and back to black as it leaves.

We call the observe() method on the IntersectionObserver object to make the Observer start watching the element for an intersection.

In the demo below, the white area with the scroll bar represents the viewport. The gray part indicates areas on the page that are outside the viewport and not normally visible in the browser.

Watch how the color of the box changes as soon as one single pixel of it enters the viewport:

The element changes color once a single pixel of it enters the viewport.
The element changes color once one single pixel of it enters the viewport.

Intersection Observer options

Apart from the callback function, the IntersectionObserver() constructor also accepts an options object we use to customize the conditions that must be met for the callback to be invoked.

threshold

The threshold property accepts a value between 0 and 1 that specifies the percentage of the element that must be visible within the viewport for the callback to be invoked. It has a value of 0 by default, meaning that the callback will be run once a single pixel of the element enters the viewport.

Let’s modify our previous example to use a threshold of 1 (100%):

index.js

const observer = new IntersectionObserver(
  (entries) => {
    for (const entry of entries) {
      const intersecting = entry.isIntersecting;
      entry.target.style.backgroundColor = intersecting ? 'blue' : 'black';
    }
  },
  // 👇 Threshold is 100%
  { threshold: 1 }
);

const box = document.getElementById('box');

observer.observe(box);

Now the callback that changes the color will only be executed when every single pixel of the element is visible in the viewport.

The element changes color once every single pixel of it enters the viewport.
The element changes color once every single pixel of it enters the viewport.

threshold also accepts multiple values, which makes the callback get each time the element passes one of the threshold values set.

For example:

index.js

const threshold = document.getElementById('threshold');

const observer = new IntersectionObserver(
  (entries) => {
    for (const entry of entries) {
      const ratio = entry.intersectionRatio;
      threshold.innerText = `${Math.round(ratio * 100)}%`;
    }
  },
  // 👇 Multiple treshold values
  { threshold: [0, 0.25, 0.5, 0.75, 1] }
);

const box = document.getElementById('box');

observer.observe(box);

We pass 5 percentage values in an array to the threshold property and display each value as the element reaches it. To do this we use the intersectionRatio property, a number between 0 and 1 indicating the current percentage of the element that is in the viewport.

The text is updated each time a percentage of the element in the viewport reaches a certain threshold.
The text is updated each time a percentage of the element in the viewport reaches a certain threshold.

Notice how the text shown doesn’t always match our thresholds, e.g., 2% was shown for the 0% threshold in the demo. This happens because when we scroll fast and reach a threshold, by the time the callback can fire to update the text, we have already scrolled in more of the element beyond the threshold.

If we scrolled more slowly the callback would have time to update the text before the element scrolls past the current threshold.

rootMargin

The rootMargin property applies a margin around the viewport or root element. It accepts values that the CSS margin property can accept e.g., 10px 20px 30px 40px (top, right, bottom, left). A margin grows or shrinks the region of the viewport that the Intersection Observer watches for an intersection with the target element.

Here’s an example of using the rootMargin property:

index.js

const observer = new IntersectionObserver(
  (entries) => {
    for (const entry of entries) {
      const intersecting = entry.isIntersecting;
      entry.target.style.backgroundColor = intersecting ? 'blue' : 'black';
    }
  },
  // 👇 Root margin 50px from bottom of viewport
  { rootMargin: '50px' }
);

const box = document.getElementById('box');

observer.observe(box);

After setting a rootMargin of 50px, the viewport is effectively increased in height for intersection purposes, and the callback function will be invoked when the element comes within 50px of the viewport.

The red lines in the demo indicate the bounds of the region watched by the Observer for any intersection.

The element changes color when it comes within 50px of the viewport.
The element changes color when it comes within 50px of the viewport.

We can also specify negative margins to shrink the area of the viewport used for the intersection.

const observer = new IntersectionObserver(
  (entries) => {
    for (const entry of entries) {
      const intersecting = entry.isIntersecting;
      entry.target.style.backgroundColor = intersecting ? 'blue' : 'black';
    }
  },
  // 👇 Negative margin
  { rootMargin: '-50px' }
);

const box = document.getElementById('box');

observer.observe(box);

Now the callback is fired when a single pixel of the element is more than 50px inside the viewport.

The element changes color when a single pixel of the element is more than 50px inside the viewport.
The element changes color when a single pixel of the element is more than 50px inside the viewport.

root

The root property accepts an element that must be an ancestor of the element being observed. By default, it is null, which means the viewport is used. You won’t need to use this property often, but it is handy when you have a scrollable container on your page that you want to check for intersections with one of its child elements.

For instance, to create the demos in this article, I set the root property to a scrollable container on the page, to make it easy for you to visualize the viewport and the areas outside it and gain a better understanding of how the intersection works.

Second callback parameter

The callback passed to the IntersectionObserver() constructor actually has two parameters. The first parameter is the entries parameter we looked at earlier. The second is simply the Observer that is watching for intersections.

const observer = new IntersectionObserver((entries, o) => {
  console.log(o === observer); // true
});

This parameter is useful for accessing the Observer from within the callback, especially in situations where the callback is in a location where the Observer variable can’t be accessed, e.g., in a different file from the one containing the Observer variable.

Preventing memory leaks

We need to stop observing elements when they no longer need to be observed, like when they are removed from the DOM or after one-time scroll-based animations, to prevent memory leaks or performance issues.

We can do this with the unobserve() method.

new IntersectionObserver((entries, observer) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      doAnim(entry.target);
      observer.unobserve(entry.target);
    }
  });
});

The unobserver() takes a single element as its argument and stops observing that element.

There is also the disconnect() method, which makes the Observer stop observing all elements.

Conclusion

Intersection Observer is a powerful JavaScript API for easily detecting when an element has intersected with the viewport or a parent element. It lets us implement lazy loading, scroll-based animations, infinite scrolling, and more, without causing performance issues and having to use complicated logic.



Every Crazy Thing JavaScript Does

A captivating guide to the subtle caveats and lesser-known parts of JavaScript.

Every Crazy Thing JavaScript Does

Sign up and receive a free copy immediately.

Leave a Comment

Your email address will not be published. Required fields are marked *