The Intersection Observer API: Practical Examples

Ryan Finni
6 min readAug 25, 2019

This article demonstrates using the Javascript Intersection Observer API, instead of measuring scroll position, to perform common visibility detection tasks.

The Problem

In Javascript, performing an action when an element becomes visible to a user has previously always required the following:

  1. Detecting the current scroll position. Setting an event listener to do this is easy enough, but the performance overhead can be costly. You will almost always want to write a throttling function to limit the amount of events fired when a user is scrolling. If you don’t know how to write this yourself, then you might reach for a library like Lodash, etc. which adds another dependency to your codebase.
  2. Getting the top offset. Select the element you want to observe in the DOM and get its top offset relative to the element (or viewport) you are detecting. This may seem straightforward until you factor in any lazy loaded or async loaded content. Once you finally have that number, don’t forget to recalculate it because that pesky banner image loaded in at the last second and skewed your original number.
  3. Unset and cleanup. Once you run your logic, you need to remember to remove the event listener. Sometimes you may have several.
  4. Beware of main thread work. The function you pass to the event listener will be running on the main thread, and will be running hundreds or possibly thousands of times until your condition is met, even with the throttling you hopefully put in place.
  5. Other use cases. Last but not least, there are scenarios where you may want to detect when an element is about to become visible, or after a user has scrolled past an element by a certain threshold. This would require more changes to the logic above.

The Solution

The Intersection Observer API is an excellent solution to this problem. It’s a fairly recent browser API that lets developers hand most of these tasks off to the browser, in a way that is more optimized.

“The Intersection Observer API provides a way to asynchronously observe changes for the intersection of a target element with an ancestor element or with a top-level document’s viewport.” — MDN Docs

This means less code to write, and a lot less overhead to worry about. Intersection Observer allows you to call a function when a target element “intersects” another element. This target element can be anything, but is most commonly (and by default) the viewport.

In order to see how this works, let’s walk through three practical examples to help get a better understanding of this API. We will mostly focus on the Javascript that’s involved, but please check the Codepen examples for a full working demo.

Example 1 — Basic Content Transitioning

One of the most common needs for detecting element visibility is for animating or transitioning content. For this example, let’s say we want to animate some text when it becomes visible.

To start, we first need to select an element to transition.

const target = document.querySelector('.animated');

We then write a callback function that does something when our element becomes visible, or “intersects”. In this case, we just want to add or remove a class to handle our transition. The callback receives an array of entries as a parameter.

function handleIntersection(entries) {
entries.map((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('visible')
} else {
entry.target.classList.remove('visible')
}
});
}

Because we are dealing with an array, we need to loop over each of its items and check if they intersect. If so, we add a CSS class.

Next, we want to create a new Intersection Observer instance and pass it the callback function we just wrote.

const observer = new IntersectionObserver(handleIntersection);

Finally, we tell the observer to start watching for the target element that was selected above.

observer.observe(target);

With these few steps, we can add transitions when an element becomes visible.

Example 2 — Image Lazyloading

Another common use case of detecting element visibility is to lazy load images only when they are visible. This is not a new concept, but it has always relied on measuring a user’s scroll position.

Just like in the first example, we need to select the elements we want to track. In this example there are more than one, so we use querySelectorAll to select all images with a particular class.

const images = document.querySelectorAll('.lazyload');

Next we need to write another callback function to handle our intersection. This function is similar to the first example, with a few differences.

function handleIntersection(entries) {
entries.map((entry) => {
if (entry.isIntersecting) {
entry.target.src = entry.target.dataset.src;
entry.target.classList.add(‘loaded’)
observer.unobserve(entry.target);
}
});
}

In our HTML image tags, notice that instead of adding sources on the images, we add a data-src attribute instead.

<img data-src="https://images.unsplash.com/photo-1566628356163-9ec0e4069181?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=667&q=80" class="lazyload"/>

This allows us to defer loading the images until they’ve become visible. Once they are visible, we just set the data-src value to the source value, and this will then trigger the image to start loading.

We also want to add a CSS class of loaded onto the image to give it a fade-in effect. Finally, we tell the observer to stop watching for this image because we don’t need to do anything else if it becomes visible again.

Next create a new Intersection Observer instance just as we did before.

const observer = new IntersectionObserver(handleIntersection);

Finally, tell the observer to watch the images. Since the querySelectorAll we wrote above gives us back a Nodelist of elements, we need to iterate over them and observe each element individually.

images.forEach(image => observer.observe(image));

Example 3 — Analytic Tracking

A third use case might be for analytic or metric tracking. Let’s say we want to know if users have scrolled at least 3/4 of the way through the content, and we only want to measure this action once per page.

Like like the other examples, first select a target to observe. This time, since we have a page of content, let’s focus on the last section.

const target = document.querySelector('.section:last-child');

Now we want to specify some configuration options to pass to the observer.

const options = {
threshold: 0.75,
}

The threshold value specifies the percentage of overlap that we want observed until the callback is fired. For example, a value of 0.5 means that the callback would be fired after half of the target element has been scrolled through.

function handleIntersection(entries) {
entries.map((entry) => {
if (entry.isIntersecting) {
console.log('Log event and unobserve')
observer.unobserve(entry.target);
}
});
}
const observer = new IntersectionObserver(handleIntersection, options);observer.observe(target);

This code may look familiar by now. We create the handleIntersection function, instantiate the observer, pass in the callback and options we specified above, and start observing the target we selected.

Note how we call unobserve on the target once we’ve intersected. This is so that an analytic event is only sent once.

Important Note

Because the handleIntersection callback is run on the main thread, be careful to only perform logic that is absolutely necessary and keep this logic lean.

Caveats

All of this sounds great, right? You’re probably wondering, what’s the catch?

While the Intersection Observer API is currently supported by all major browsers, you may need to add a polyfill or write alternate logic if you need to support IE11 or below.

Be sure to always add some feature detection code around any Intersection Observer logic you write, to help prevent errors in unsupported browsers.

if(‘IntersectionObserver’ in window) {
<Intersection Logic Here>
} else {
<Fallback Logic Here>
}
ORif(typeof window.IntersectionObserver !== 'undefined') {
<Intersection Logic Here>
} else {
<Fallback Logic Here>
}

Summary

As we’ve seen from these examples, the Intersection Observer API is easy to use and can make checking for element visibility much more performant and a lot less complicated.

Edit: If you enjoyed this article, you might like some of the other posts I’ve been writing on my newly launched blog.

--

--