Dec 13

Intersection Observer

Use the Intersection Observer Web API as a performant way to track where elements are in the viewport and other scrollable regions.

By Stephanie Eckles

What is the Intersection Observer API?

The Intersection Observer (IO) Web API is an asynchronous way to watch the visibility of target elements either within their ancestors or the main viewport. You can then trigger additional events based on the target element's visibility and position - aka where it's "intersecting". Importantly, this "observation" happens off the main thread (that's the async part) meaning it can be a more performant alternative to other scroll tracking methods.

While IO is technically an experimental API, it has very good browser support. Being experimental mostly means it is possible that it could undergo breaking changes. This is good to be aware of, but the core feature set can absolutely start being used today.

IO core features and concepts

Let's consider a scenario where an IO is useful: a sticky table of contents for in-page links where you want to highlight the current section.

Previously, you may have taken measurements between the top of the viewport and the top of each new section, then set up scroll tracking to add a class to the related link. With IO, we can instead separately observe when each page section intersects say the midpoint of the viewport and then trigger updating an "active" link state.

When using IO, the target element's intersection is calculated based on three option parameters:

Once the target is visible based on these parameters, the IO initiates a callback with additional data you can use for your event. One interesting bit of data is the time in milliseconds between when an observer was started and when the callback was triggered.

A gotcha for IO is the threshold value. If you select 1, meaning when the element is 100% visible, then the entirety of the element must be visible within the root at once. Given scrollable areas ranging from phones to mega monitors with varying resolution aspect ratios, 100% is difficult to achieve without failing in some scenarios. The default for threshold is 0, meaning as soon as even 1 pixel is visible the callback will initiate. A more reliable way to change when a callback occurs is to adjust the rootMargin to reposition at what point the target element needs to intersect the root.

Creating a basic Intersection Observer

First, we'll make sure the API is available:

if ('IntersectionObserver' in window) {
// IO code here
}

Then we'll initiate the observer, and begin observing our target:

// Target to observe
const boxEl = document.getElementById('box');
// Observer
const boxObserver = new IntersectionObserver(callback, options);
// Observing #box
boxObserver.observe(boxEl);

For the callback function, we'll receive an array of entries regardless of the number of target elements. In our example, we're only worried about a single entry, so we can save that by the zero index. We'll ensure it's intersecting, remove the observer, and perform our event.

const callback = (entries) => {
const box = entries[0];
if (!box.isIntersecting) return;
boxObserver.unobserve(box.target);
boxEl.textContent = 'intersected';
boxEl.style.backgroundColor = 'yellow';
}

Removing the observer is best if you only need to trigger the callback once. In the scenario of our sticky table of contents, we would want to keep the observer active.

Due to the setup of this tutorial page, we need to scope our observer root to the demo scrollable element of box-root. Then we'll also adjust the rootMargin to pull the bottom up to 50%, meaning our callback won't trigger until the box has scrolled above the midpoint of the root element.

const rootEl = document.getElementById('box-root');
const options = {
root: rootEl,
rootMargin: '0% 0% -50% 0%'
}
Basic Intersection Observer
#box-root {
height: 200px;
overflow-y: auto;
border: 1px solid red;
padding: 1rem;
font-size: 1.5rem;
}
#box {
margin: 300px auto;
border: 2px solid black;
height: 4rem;
display: grid;
place-content: center;
}
.midpoint {
position: sticky;
top: 50%;
width: 100%;
color: red;
text-align: center;
transform: translateY(-50%);
}
if ('IntersectionObserver' in window) {
const boxEl = document.getElementById('box');
const rootEl = document.getElementById('box-root');

const options = {
root: rootEl,
rootMargin: '0% 0% -50% 0%'
}
const callback = (entries) => {
const box = entries[0];
if (!box.isIntersecting) return;
boxObserver.unobserve(box.target);
boxEl.textContent = 'intersected';
boxEl.style.backgroundColor = 'yellow';
}

const boxObserver = new IntersectionObserver(callback, options);
boxObserver.observe(boxEl);
}

Scroll...

midpoint
...

Ideas for using Intersection Observer

Besides the sticky table of contents tracking (possibly also familiar to you as "scroll spy"), here are some other ideas.

Additional resources