Intersection Observer Basics

Chapter 9 27 mins

Learning outcomes:

  1. What are intersection observers
  2. Working with observers
  3. Understanding the callback function
  4. A simple example

Introduction

In the previous Introduction chapter we had a detailed discussion on how the IntersectionObserver API came into being and that what purpose it serves in modern-day web applications.

Not only this, but we also saw some of the common terms used by the API's specification and exactly how does it operate using all those concepts.

If you haven't read that chapter, please consider reading it thoroughly before you begin with this one - because in this chapter we will be building on those concepts.

Specifically, we'll see how to create an observer to monitor given elements and then a ton of details of the whole IntersectionObserver interface, including its methods.

So let's begin!

What is IntersectionObserver?

IntersectionObserver is an API developed solely for the purpose of monitoring the appearance of a given element into the viewport, or another element. It just isn't meant to do any other thing!

According to the spec, an IntersectionObserver observes elements (rather than saying monitors elements).

An intersection observer can observe as many elements as one desires using the observe() method as we shall see later on.

Talking about the name of the API, the word 'intersection' comes from the fact that this API only looks for intersections i.e when an element known as the target cuts through another element known as the root.

The element we wish to monitor is known as a target of the observer.

For example, in lazy loading we want to know when a given image enters the viewport; likewise in this case the image is our target.

On the other hand,

The element with respect to whom we wish to monitor other elements is known as the root.

For example, in lazy loading we want to know when an image enters the viewport; likewise in this case the viewport is the root. It's the region relative to whom all our calculations shall be made - where the images have to show up.

The IntersectionObserver API comes into action as soon as a target intersects with the root.

To sum up all the discussion uptil now:

The API observes each target for when it starts to intersect the root.

Hence, the name IntersectionObserver - it observes for intersections of the target and the root.

Moving on, once IntersectionObserver is sure that an intersection has occured, it executes a function known as the callback.

Now sometimes, we might want to observe when a target appears completely in the root, or when it is just touching the root, or even when it appears close to half of its height.

It isn't always desirable to fire the callback at some default and fixed value, like when the target appears completely in the root - we ought to be able to change this to meet our needs.

This is known as the intersection ratio.

The intersection ratio is a number between 0 and 1, that indicates how much of a target appears in the root.

Can you guess how it's calculated?

Height (or width) of the intersection rectangle divided by the height (or width) of the target's bounding rectangle.

If the intersection ratio is 1 it means that all of the target is visible in the root. Similarly, if it's 0, it simply means that the target is not even a single pixel visible in the root.

Developers, can specifically craft an intersection observer to observe for given intersection ratios only. This is known as the threshold of the observer.

The threshold is a list of intersection ratios for which to observe the target.

For example, in lazy loading we would rather go with a threshold of only 0 to load an image as soon as it's just about to enter the viewport.

An intersection ratio of 0 can mean two things: one that the target is completely away from the root OR that it's merely touching either of the root's edges.

Don't worry if you can't understand this - we'll see it later on in this unit.

In contrast to this, in ad analytics we would rather go with a threshold of 1 to confirm that an ad banner was completely viewed by the user, before logging an impression count.

It just depends on the application of what threshold it requires. A threshold can even define more than one intersection ratio, as we shall see in the next section.

Threshold is a list of intersection ratios - not an intersection ratio itself. Remember this!

With all the necessary details covered on what's the architecture of the IntersectionObserver API, let's now go ahead and see how to get into business with it.

Working with observers

It all starts by instantiating an object out of the IntersectionObserver() constructor, along with passing it some arguments:

var io = new IntersectionObserver(callback, options)

Let's see what the arguments are...

As we've said before, an observer needs to have a callback function in reserve, which it can execute once any of its targets intersects with the root.

This callback goes as the first argument to the IntersectionObserver() constructor. We'll see what's the format of this function later on.

The callback is fired each time when a target's intersection ratio goes beyond the provided threshold and when it goes lesser than it.

Apart from this, an observer must also have some information to know exactly how to observe its targets i.e shall it look for when they intersect completely with the root, or in half proportions and so on..

This information goes as properties of an options object, passed as the second argument to the IntersectionObserver() constructor.

The properties are:

  • root: The root element of the observer. Defaults to null, which means that the root is the viewport.
  • rootMargin: A string to indicate the margins around the root, similar to the CSS margin property. Defaults to "0" i.e no margins.
  • threshold: An array of intersection ratios to observe. For example, to observe for the ratios 0 and 1, we'll assign the array [0, 1]. Defaults to [0].

As you can see, each property has a default value and so one can safely omit the options argument if the defaults serve his needs.

Anyways, reaching this far isn't the end of the game - we have to yet tell the observer which elements we wish to be observed. This is done using the observe() method.

It takes an element node as an argument that shall be put in observation.

Or better to say, it takes an element node as an argument and makes it the target of the corresponding observer. Be familiar with these special words!

Let's see a very basic example.

Below we create a <div> and apply CSS styles in order to ensure that it is sufficiently far from the top of the document. The goal is of making a console log once this div appears completely in the viewport.

<div>A div</div>
div {
   margin: 1000px 0 300px;
   background-color: #ccc;
   padding: 40px;
}

After this, we start by selecting the <div> in a variable target and then create an IntersectionObserver instance:

var target = document.getElementsByTagName("div")[0];

function callback() {
   console.log("OK");
}

var options = {
   root: null,
   threshold: [1]
}

var io = new IntersectionObserver(callback, options);

Note that as we stated above, the goal is of making a log once the div appears completely in the viewport; hence:

  • callback will be a function with the statement console.log("OK") in it. In this case we use a named function callback().
  • root will be the value null because we are concerned with the viewport (which is actually redundant to mention since null is the default).
  • threshold will be the array [1], since we want to know when the element appears completely in the viewport.

With this done, we finally observe the element by passing it to the observe() method, as shown below:

var target = document.getElementsByTagName("div")[0];

function callback() {
   console.log("OK");
}

var options = {
   root: null,
   threshold: [1]
}

var io = new IntersectionObserver(callback, options);
io.observe(target);

And this completes the whole logic of our very basic intersection observer. Check it out in the link below.

Live Example

As soon as you open the link in another tab, you'll get a log right away even though the div element won't be even a pixel near the viewport. This is because the intersection observer executes the callback as soon as the page loads.
As we shall see in the next section, there is a way to know exactly when is the callback fired at the intersection ratio we desire.

Now that we've covered the basics of intersection observers, it's time to build the real skills of working with them.

Calling the callback

Verily, the most important thing to understand in intersection observers is the callback function which is passed as the first argument to the IntersectionObserver() constructor.

There are essentially two important things regarding the callback that you ought to understand. They are:

  1. When is the callback exactly fired.
  2. What argument is provided to the callback.

If you understand both these things, believe it or not, you have actually understood almost all the theory of observers!

So let's start with the first concern: when is the callback fired.

We'll take the help of the same example shown above. Just to recap it - a log is to be made once the div element appears completely in the viewport. root is null, and threshold is [1].

But before we dissect this example, we have to first explore how's threshold related to the execution of the callback.

The value [1] basically means that the intersection ratio we wish to observe the target for is 1 i.e when the target shows up completely in the root.

This is one instance where the callback would fire. However it isn't the only instance.

The callback function essentially fires on two occasions: when the intersection ratio of the target goes,

  • greater than (or equal to) a threshold value,
  • lesser than a threshold value.

In our case, the moment the div appears completely in the viewport, the respective callback gets fired; since the intersection ratio (which is 1) becomes equal to the threshold value 1.

Similarly when the div leaves the viewport, then also does the callback get fired; since the intersection ratio is now something lesser than 1 (like 0.95, 0.8, 0.1, 0.003 etc.).

If the intersection ratio goes greater than a threshold value which was previously already exceeded, then the callback isn't fired. Same goes for the lesser-than case.

For example, if the threshold is 0.5 and we go to an intersection ratio of 0.6, a callback will indeed be fired. Now if we go to an intersection ratio of 0.7 nothing will happen simply because we've already exceeded the threshold 0.5.

Now you maybe thinking what's the point of firing the callback when we go lesser than a threshold value. Well let's think of it very naturally.

An ad analytics library would definitely need to know when an ad banner appears fully in the viewport in order to count an impression. However, it will also need to know when it exits the view so that it can, for instance, calculate the time spent viewing the ad.

In another instance:

Suppose that a program fades in some elements as they enter the viewport and fades them out as they leave the viewport.

For the former, we'll need to know when the intersection ratio goes greater than a threshold value; whereas for the latter we'll need to know when it goes lesser than a threshold so that we can easily set each element's opacity to its intersection ratio.

In short: both the cases matter - when a target's intersection ratio goes greater than (or equal to) a threshold value and when it goes lesser than a threshold value.

An observer's callback shall take care of both these cases.

As we shall see later on in this section, there is a way to determine which case fired the callback i.e did it fire when the intersection ratio went beyond a threshold value or when it went behind it.

One extremely important thing to note over here is that the callback function initially fires with the page load.

The reason why this happens is left to be discussed in the next section. For now you just need to remember this fact.

Whatever root you have, whatever threshold, whatever target and wherever it is; the callback function is fired for the first time with the page load.

With all this mess of explanation in place, let's take a quick observer example.

Suppose we have the same div as before, just this time the threshold of the observer is changed to the array [0, 0.5, 1], instead of [1]:

var target = document.getElementsByTagName("div")[0];

function callback() {
   console.log("OK");
}

var options = {
   root: null,
   threshold: [0, 0.5, 1]
}

var io = new IntersectionObserver(callback, options);
io.observe(target);

Open the following link and follow the steps shown below.

It's important that you scroll slowly in order to notice each of the logs as they are made!

Live Example

As soon as you open the document and its console, you'll right away notice a log in the console saying "OK". This is because the callback fires the first time with the page load.

Now scroll slowly such that you just bring 1 or 2 pixels of the div into the viewport. You'll notice another "OK" log in the console.

Now once again, gradually scroll slowly until you feel that half of the div is visible. Continue scrolling (slowly) and you'll see another log "OK".

Finally scroll until the div shows completely in the viewport. At this stage you'll witness the third "OK" log.

Go back all these steps slowly and you'll get three further "OK" logs.

What's simply happening here is that in the first three scrolls we are exceeding the threshold values 0, 0.5 and 1, one-by-one in this order; and so we get the callback fired for each case.

Furthermore in the last three scrolls we are going lesser than the threshold values 1, 0.5 and 0, one-by-one in this order; and so we get a callback fired for each case.

Altogether we get 7 logs.

If you didn't get 7 logs, refresh the page and redo your document's scrolling.
If even after a refresh, you don't get 7 logs, then you need to first understand the callback in detail in the section below.

Thing to note!

One crucial thing to remember in this regards is that:

If for a given target, a scroll change leads to an intersection ratio that is greater than (or equal to) or lesser than multiple threshold values, then the callback will be fired just once - NOT for each threshold value.

For example, if our threshold is [0, 0.5, 1] and our current intersection ratio is 0, changing to a ratio of 1 would fire the corresponding callback only once - not 3 times.

This means that if we use the scrollTop property to bring our div (in the example above) into and out of view in just one go, only two callback executions will be made - NOT 6.

The link below illustrates this very example.

Live Example

A button serves to change the scroll position of the document so as to bring the target immediately into and out of the viewport. The respective callback fired, writes to an element to indicate of the change right on the document window.

So simple!

Now if you've really understood the discussion above, try solving the following question to test out yourself.

Callback's arguments

If you've not been able to understand a word in the discussion above, then you might fall in the category of those developers who need to first understand the structure of the callback - what arguments does it receive.

This is what we shall cover in this section.

Let's dive in...

When an observer fires its callback, there are numerous pieces of information one might want to know while executing the function.

For example, one might want to get the time at which the intersection occured, or the element which intersected with the root, or even the current dimensions of the target and the root.

The IntersectionObserver API indeed provides all these pieces of information to the callback by means of an argument.

Shown below is the structure of the callback function:

function callback(entries, observer) {
   // definition of callback
}

There are two arguments provided to the callback when it's invoked by an observer.

  1. The first entries argument is an array of IntersectionObserverEntry objects. We'll explore this in the section below.
  2. The second observer argument is simply a reference to the observer object (available via this as well).

Let's start by exploring both these arguments on the outskirts.

Consider the example below. We have a div being observed for the threshold [1] (which you now know means what!):

<div>A div</div>
The CSS for this example is the same as the first example above.
var target = document.getElementsByTagName("div")[0];

function callback(entries, observer) {
   console.log(entries instanceOf Array); // true
   console.log(observer === this); // true
}

var io = new IntersectionObserver(callback, {threshold: [1]});
io.observe(target);
We've omitted the root property in the options argument here - since it defaults to (you know it!) null!

What's new this time is the callback function. It logs some important characteristics of its two arguments.

The expression entries instanceOf Array return true always which confirms the fact that the first arg is an array.

Moreover, observer === this also returns true which means that to get the observer of the callback we can either use its second argument or the this keyword - both point to the same object.

Live Example

Alright, now it's time to explore the truth of entries.

As we already know, the first argument of the callback, that is entries, is an array. An array whose elements are IntersectionObserverEntry objects.

But what exactly is an IntersectionObserverEntry object?

Hmmm, now this is something interesting!

An IntersectionObserverEntry object holds information regarding the intersection of a given target of the observer.

One such object holds information regarding the intersection of one target; likewise for multiple targets we have an array of these objects, where each IntersectionObserverEntry element holds information for each target.

The information is in the form of the following properties:

  1. intersectionRatio - a number between 0 and 1 (both inclusive) to indicate the ratio of the intersection between the target and the root.
  2. isIntersecting - a Boolean value to indicate whether intersectionRatio is greater than (or equal to) a threshold value or lesser than one.
  3. target - the current target element.
  4. time - a number representing the time (in milliseconds) since the page load, at which the target cut past a threshold value.
  5. boundingRect - the bounding box of the target (excluding any scrollbars).
  6. intersectionRect - the bounding box of the intersection rectangle (excluding any scrollbars).
  7. rootBounds - the bounding box of the root (excluding any scrollbars).

Although, we'll cover all these properties, in detail, separately in the next chapter, for now we will demonstrate an simple example using two of them - target and intersectionRatio.

Suppose we have a div (as before), being observed for the threshold [0, 1].

Once the callback fires, the idea is to take the intersection ratio of the div and assign it to its opacity property, to give a fade-in effect as it appears into the viewport.

Shown below is the code to accomplish this:

<div>A div</div>

The following CSS styles are applied simply to give the div a nice feel, and most importantly in the case of the transition property, to ensure that the opacity change occurs smoothly - not all in one go!

div {
   /* ensure smooth opacity change */
   transition: 0.5s linear;

   /* make a div look like a square */
   padding: 50px;
   display: inline-block;

   /* make it look elegant! */
   background-color: blueviolet;
   color: white;
   font-size: 20px;

   /* make sure it's far away from the viewport */
   margin: 1000px 0;
}
To learn the CSS transition property in detail please consider reading CSS Transitions Introduction.
var target = document.getElementsByTagName("div")[0];

function callback(entries) {
   var entry =  entries[0];
   entry.target.style.opacity = entry.intersectionRatio;
}

var options = {
   root: null,
   threshold: [0, 1]
}

var io = new IntersectionObserver(callback, options);
io.observe(target);
The reason as to why we've given the threshold [0, 1] here and not [1] will be discussed in the next chapter.

Live Example

Looking at the threshold we can clearly see that the callback here fires on two occasions: one when the intersection ratio of the div is equal to 0 and one when it is equal to 1.

In both cases, we take the ratio and assign it to the opacity property of the div. The 0-threshold case is redundant, given solely for resetting the div to its original opacity.

On the otherhand, the 1-threshold case is given to fire the callback when the div is completely visible in the viewport, and thus give it an opacity of 1 - ultimately fading it in.

Finer details of intersectionRatio and all the properties discussed above will be covered in the next chapter.

Yup, that's right - all properties!