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:
root
: an ancestor element used for tracking visibility, defaulting to the browser viewportrootMargin
: enables changing the intersection bounding box using percent or pixel values corresponding totop right bottom left
similar to margin, useful for tracking based on passing for example the midpoint of the ancestor vs. the bottom edgethreshold
: the percentage of the target element that must be visible to trigger the callback, defined as a single number or an array of decimal values between0
and1
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...
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.
- analytics events involving visibility or read time
- triggering animations
- lazy loading assets and scripts
- closing out-of-view menus, tooltips, and overlays
Additional resources
#- MDN documentation
- I explored a few different tracking methods in the context of gathering analytics events
- Travis Almand provides a deep investigation into IO
- Preethi provides several practical use cases for IO like lazy loading or pausing out-of-view videos