Understanding The Intersection Observer API
Published on
In this post, I'll explain the concept behind the browser's native IntersectionObserver API and how to use it. I'll also show a real-world use case in React - infinite scrolling.
Prerequisites
This post assumes familiarity with the following topics:
- React, particularly the hooks - useEffect and useRef.
- Basic understanding of viewports.
- Callbacks in JavaScript.
- Basic CSS.
Visualizing The Concept
I have built an interactive demo below to help me explain the Intersection Observer API. Imagine a web page as the box with a dashed outline as below. Within it, we have an element represented by the balloon.
A webpage.
Also, imagine the browser's viewport as a box with a solid outline as below. We need a scrollbar to visualize how this works, hence I've made the page longer than the viewport.
The viewport.
Use the scrollbar next to the page to scroll the page and see what you can understand. This is just to help you get familiar with the idea before I explain it in detail. π
Click the "Start Observing" button to begin. The π€¨ emoji will appear, indicating that we're in the observing state. Now, use the slider on the right to scroll the page and watch for the π emoji to appear at key moments. Take note of the exact states when it shows up.
Based on your observation, see if you can answer the quiz below. You don't have to get them right, the important thing is that you make an attempt. This will help you understand the concept better.
The π emoji appears at 3 distinct states:
- As soon as we start observing.
- When the balloon first enters the viewport when we're observing.
- When the balloon first exits the viewport when we're observing.
Technically, the balloon in our example is called the target (it's the element we want to observe), and the viewport is called the root.
The first state indicates that we started observing the target. This can be used to learn the initial
intersection status, before any scrolling occurs.
The second state indicates that at least one pixel of the target overlapped with at least one pixel of the root; we say the target intersects with the root. The third state indicates that no pixels of the target overlapped with the root; we say the target doesn't intersect with the root.
Let's look at some code!
The API
Here's an implementation of the above behaviour in React. Play around by scrolling the page and see if you can figure out what's going on. I'll break down the parts right below the playground. Try scrolling the balloon in and out of view and compare it with the interactive demo above. Clear the console and reload the page to observe the behavior from the beginning.
We create an Intersection Observer using its constructor:
The callback
will be invoked by the observer
when something interesting happens. Our callback simply logs "Interesting". In the interactive demo, this callback invocation was represented by the emoji π appearing.
How does the observer
know what is interesting? We have two knobs to tell the observer what we are interested in.
The First Knob: Specifying the target
The first one is to specify the target:
Now that the observer
is made aware of the target, it starts observing it. This is represented by the "π€¨ Start Observing" button in the interactive demo.
We can make the same three observations from this code as in the interactive demo:
- "Interesting" is logged when the page first loads. (This actually happen when we first call the
observe
function on theobserver
. It just appears to happen on page load.) - It's logged when the first pixel of the target enters the viewport.
- It's also logged when last pixel of the target exits the viewport.
This is the default behaviour of the IntersectionObserver
when we do not use the second knob.
The entries
Argument
Our callback
is invoked with the entries
argument by the observer
. This argument is an array where each item describes the intersection event that caused the callback
to be invoked. We ignored the event in the previous example. In the following example, we use it to trigger an animation when the target enters the viewport.
Let's look at the entries
argument:
We pluck the one and only item from the entries
array. How do we know it has only one element? Because we have observed only one element. (When we observe multiple elements or tweak the second knob in certain ways, the entries
argument can have multiple events. We won't cover that in this blog post.)
Each intersection event is described by a set of properties. For example, the isIntersecting
property tells us if the target entered or exited the viewport when the event occured. We conditionally apply the scaleUp
CSS class whenever the target (the balloon in our case) is visible. This triggers the animation exactly when the target enters the viewport! We detect visibility by checking if isIntersecting
is true
. When it's false
, it means the target is not visible (in other words, it's not intersecting).
The Second Knob: The Options Object
The second knob to control what we're interested in is the second argument to the IntersectionObserver
constructor. It's an object with three optional properties.
We'll look at them in turn.
root
Root is the element that acts as the viewport for the intersection calculation. In the above examples we never set the root explicitly; so it defaulted to the browser viewport. In the context of the playground, the default root ended up being the iframe's viewport. The below sandbox shows an example where the root is a different scroll container.
rootMargin and threshold
rootMargin
and threshold
adjust the boundaries of the root and the target, respectively, used to calculate the intesection. I've added additional controls to the following interactive demo to show how these two properties work. Play around to see what you discover. π
Tweak the root margin and the threshold values before observing to alter the edges used in the intersection calculation. Root margin can take values for all 4 edges like the normal margin property, I've showed only the bottom edge to keep it simple. The threshold can also be set to any decimal value between 0 and 1. I have restricted it to keep it simple.
In our previous examples, the default rootMargin
was 0
on all sides and the default threshold
was 0
. Let's consider a different example using the above demo. Set the threshold to 1.0
(leaving the rootMargin
at 0
) and start observing. Now if you scroll, we won't see a reaction when the first pixel of the balloon enters the viewport. Instead, we see a reaction when the last pixel enters the viewport.
As another example, stop observing, set the rootMargin
to 40px
, threshold
to 0
and scroll the page to the top. Now start observing and start scrolling the balloon into view. The reaction will trigger way before the balloon enters the viewport. More specifically, the reaction triggers when the first pixel of the balloon touches the 40px
margin's edge.
An Exercise
In our previous example the balloon scaled up too early, before it was fully in view. It appears as though the balloon is peeking from below.
What if our goal is to clearly show the balloon is scaling up?
I've added the same sandbox below. Can you try to use the options
we just learned about to achieve this?
Solution
The solution is to add a threshold when creating the IntersectionObserver:
Infinite Scrolling
Here's an example of how we can achieve the infinite scrolling effect using the Intersection Observer.
Here, the target is a zero-height element positioned at the end of the viewport:
We can position this element anywhere in the DOM to control when we make the network request.
This example only focuses on the Intersection Observer API. In the real-world, we also have to handle error states and avoid duplicate network requests. This can be done by hiding the target in the error and loading states.
Conclusion
I hope this helped you get a better understanding of how the API works π! Head over to the official docs to explore further. The API supports multiple threshold levels, provides the exact intersection ratio, and much more. You can also find a list of use cases at the beginning of the pageβone of them being infinite scrolling.