Tracking Elements in the Viewport with the Intersection Observer API

Sam Rubin
The Storyblocks Tech Blog
5 min readJul 10, 2019

At Storyblocks, we surface millions of stock media items (videos, images, audio clips) every day. Our team is continuously improving our search algorithms to help our users find and discover the exact media they need to help tell their digital stories. This task becomes difficult when searches can yield hundreds of thousands of results.

What’s more, we recently added a lot of new content to our member library, content that needed to find a way into our search results. This set us on a path to find a better way to surface content, and more specifically, to rank the results. Our solution, introduced by our search and data science team, was “Batting Average”. As the name implies, Batting Average measures, at a high level, how many times a piece of content has been given a chance versus how many times that content was interacted with.

For Batting Average to succeed, we needed more than just the pure numbers, we needed to also “level the playing field” for pieces of content. After all, showing a piece of content in the #1 result, impacts how many interactions that piece of content is going to get, unrelated to the actual content itself. One way in which we wanted to “level the playing field” was to track when content was actually seen by the user versus appeared in a search result.

Our engineering team was tasked with delivering this data to our event pipeline to be consumed by the data science team. We were already tracking when users hovered on our videos or clicked on them, but we were missing data on when users saw our videos.

In the Storyblocks web app, we load a few dozen clips each time a user performs a search. Publishing a display event on page load would be a naive implementation since, depending on the user’s screen size, the user may only see a few of those media items on page load. A better approach would be to determine when the media item has entered the user’s viewport and fire an event at that point in time.

Storyblocks Mobile: Only 3 of 48 Clips are in Viewport on Page Load

Historically, this approach would require using event handlers, loops, and calling the getClientBoundingRect method on a web element . Since all this code runs on the main thread, even one of those items could cause performance problems by blocking the main thread. Enter the Intersection Observer API.

Intersection Observer API

The Intersection Observer API is a perfect fit for this use-case. Just look at its definition from MDN:

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

Example Code for Implementing Intersection Observer

Code

To get up and running, we create a new IntersectionObserver instance with a callback that executes when the intersection is made, and some options. Since we only care about an element being fully visible in the viewport, we can specify the options as:

const options = {
root: null, // The document viewport (default)
rootMargin: '0px', // No offset from the root element (default)
threshold: 1.0, // Full intersection into the root element
};

Then we can pass those options to the constructor along with our callback. We added some other checks in here, since when the IntersectionObserver is instantiated, the callback is run once as a detection for whether or not the element is in view and if its intersection ratio exceeds the given threshold. Since we also only care about observing the viewport entry one time, we can stop observing after an intersection is detected.

const observer = new IntersectionObserver((entries, observer) => {
if (entries[0].intersectionRatio <= options.threshold) {
return;
}
observer.disconnect();

// Our callback business logic here
const target = entries[0].target;
alert(`A new video element with id ${target.id} has entered the viewport.`);
eventEmitter.publish('DisplayEvent', target);
}, options);

The last thing we need to do is actually tell the Intersection Observer to observe a target element via the observe method, where we pass a target DOM node that we want to observe. In our case, it is the DOM node containing the stock media item.

const targetElement = document.querySelector('.stockMedia-item');
observer.observe(targetElement);

Results

If we run the code in the browser, we’ll see our events firing when the target element fully enters the viewport. We added a browser alert in the gif below to demonstrate.

Intersection Observer API Example on Storyblocks

Bonus: Creating a Reusable Module

For our implementation at Storyblocks, we decided to make a small, focused module to resolve when an element has entered the viewport at a specified threshold. This enabled us to use async/await with IntersectionObserver and to reuse the code in our other applications. The code also returns the IntersectionObserverEntry object, which has some useful information about the intersection event. You can find links to the code, package, and examples at the end of the article.

Using this module, we can now write:

import elementInViewport from ‘element-in-viewport’;(async () => {
const threshold = 1.0;
const mediaItem = document.querySelector(‘.stockMedia-item’);
const entry = await elementInViewport(mediaItem, threshold);
alert(`Intersection in Viewport Detected at IntersectionRatio ${entry.intersectionRatio}`);
})();

Much nicer! Almost all of the code we wrote earlier is baked into the package, keeping our application code clean.

Final Notes

One thing to note with the Intersection Observer API is that it’s not fully supported in all browsers (it’s just over 85% at the time of this article). However, there is an available polyfill.

Browser Support for IntersectionObserver — June 2019

Feel free to check out the links below to view reference material for the Intersection Space Observer API and links to the open source package.

Reference / Further Reading

Intersection Observer API
IntersectionObserver
Code and Example: element-in-viewport
npm: element-in-viewport

--

--

Sam Rubin
The Storyblocks Tech Blog

Full stack engineer working to deliver engaging user experiences. Currently @Storyblocks, formerly @DeloitteDigital, @DoJ