Intersection Observer Entries

Chapter 10 31 mins

Learning outcomes:

  1. The entries argument
  2. The IntersectionObserverEntry interface
  3. Application of each IntersectionObserverEntry property

Introduction

By far the most useful deal of the IntersectionObserver API is the entries argument of the callback function.

It holds an array of objects, each of which tells a whole tale about a given target element. If you know this well, you know the whole observer's API. Believe it!

In the previous IntersectionObserver Basics chapter, we saw the entries argument just on the outskirts. Now we shall unravel it in fine detail, to the core; specifically focusing on the structure of each element of the entries array.

The entries array

As we know by now, the callback function of an intersection observer is passed in two arguments the moment it's called.

If you don't know about the callback function, consider reading IntersectionObserver Basics.

The first argument is known as entries and is an array of IntersectionObserverEntry objects.

Each such object represents information about a given target's intersection with the root.

The targets that line up in the entries array are those elements whose intersection ratio has gone greater than (or equal to) a threshold value or whose intersection ratio has gone lesser than a threshold value.

This has been discussed in fine detail in the previous chapter.

For example, say we have an observer with a threshold of [1] and two targets being observed by it. Also suppose that upon scrolling the document to some value (like by using the scrollTop property), they both show up in the viewport completely.

What would happen now is that, because their intersection ratios have both become equal to the threshold value 1, they both will show up in the entries argument of the fired callback.

The callback will be fired just once, but it's first array argument will hold all the targets that have met the threshold.

Now a good question, arising in the minds of some developers, at this point is that why does the observer API use an array to address given targets - why can't it simply fire the callback for each target.

Why the API uses an entries array?

Let's see what this means...

The reason of using an array to represent all affected targets is to simply prevent the overhead of calling multiple callbacks.

The IntersectionObserver API could've surely been designed to execute the callback multiple times for each affected target; in which case the first argument of the callback would've been called entry (rather than entries).

However, due to the overhead of executing the callback multiple times for each target, and on the otherhand, the simplicity of working with the array data type, the API decided to use the latter - arrays.

In short, there isn't any special reason as to why an array is used to address all given targets, instead of executing the callback for each of them. It's just a sensible reason that calling a function multiple times takes time!

That's it!

Anyways, let's consider a real example.

We have three div elements lining one after another (which is done using CSS), being observed for the threshold [1].

<div>Div 1</div>
<div>Div 2</div>
<div>Div 3</div>
div {
    /* make divs appear in one line */
    display: inline-block;
    width: 25%;

    background-color: #ddd;
    padding: 20px;
}
var targets = document.getElementsByTagName("div");

function callback(entries) {
   console.log(entries);
}

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

var io = new IntersectionObserver(callback, options);

// observe each div element
for (var target of targets) {
    io.observe(target);
}

Live Example

As soon as the page loads, the callback gets fired with entries containing all the three targets i.e its length is 3.

Now the moment the divs appear in the viewport, the callback function gets fired once again with entries holding an array of three elements - each an IntersectionObserverEntry object representing one of div targets.

We get all the three div elements lined up in entries simply because the intersection ratios of all of them become equal to the threshold value 1 at this point.

Similarly, when the divs go even a pixel out of the viewport - the callback gets fired with entries once again holding three elements.

This is because at this point, the intersection ratios of all the div elements have gone lesser than the threshold value of 1. Hence, all of them line up in entries.

So hopefully, this ain't difficult to understand!

Moving on, let's now zoom into the indivdual elements of the entries array and see how to work with them.

A single entry

Each element of the entries array is an IntersectionObserverEntry object. You can refer to each element as an entry by the observer.

An IntersectionObserverEntry object holds intersection information for a given target.

The information can be something like the ratio of the intersection, the time at which the intersection happened, or the dimensions of the target and so on.

To tell the whole story, following are the properties exposed by an IntersectionObserverEntry object:

  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).

It's completely impossible for you to find none of these properties on a real-world application using the IntersectionObserver API.

One has to use some of these properties in order to make the observation meaningful - otherwise we'll be merely observing elements and not performing any useful task when they intersect.

Starting from the first target property, following we'll be going in the detail of each of these properties and see real examples of where we could use them.

Intersection ratio

If you need to get the ratio of the intersection between the target and the root, then you ought to use the intersectionRatio property.

It's a number between 0 and 1 (both inclusive) to indicate the ratio of the area of the target over the area of the root.

If intersectionRatio is 0, it means the target and root are not intersecting. Similarly, if it's 1 it means that the target and root are completely intersecting.

Consider the code below.

<div>A div</div>
div {
    padding: 100px;
    margin: 1000px 0;
    display: inline-block;
    background-color: #ddd;
}

We have a target div with a large area (thanks to the padding property) whose intersection ratio will be logged each time its observer's callback is executed.

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

function callback(entries) {
   console.log(entries[0].intersectionRatio);
}

var options = {
   root: null,
   threshold: [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1]
}

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

As the target is brought into the viewport, its amount of intersection increases and therefore intersectionRatio also increases.

Similarly, as the target is moved out of the viewport, its amount of intersection with the viewport decreases and therefore intersectionRatio also decreases.

To view the intersection ratio live on the document itself, visit the link below.

Live Example

An effective use case of this property follows.

Construct an intersection observer from scratch that observes the div shown below relative to the viewport and increases its opacity as its brought more and more into view.

<div>A div</div>
div {
    padding: 100px;
    margin: 1000px 0;
    display: inline-block;
    background-color: #222;
    color: white;
}

You shall use a threshold list starting at 0, ending at 1 and with intervals of 0.1.

You shall also select the div in a variable target and pass that to the observe() method as well use it inside the callback.

The question clearly defines the threshold - it starts at 0, ends at 1, and has intervals of 0.1 i.e 0, 0.1, 0.2, ..., 1

Furthermore the question also clearly states to select the div inside a variable target. The method we'll use to fetch the div is document.getElementsByTagName().

This global variable target will also be used inside the callback to apply the opacity change to the div element.

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

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

var options = {
    root: null,
    threshold: [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1]
}

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

Live Example

The reason why we are using a threshold with intervals of 0.1 is to make sure that the callback is fired quite frequently while the div is coming into or going out of the viewport, so that we can make the respective opacity change is apparent.

Another use case of intersectionRatio will be discussed together with the time property.

Is it intersecting?

Say you want to know when a given target is completely visible in the viewport.

You can easily craft an if statement to accomplish this, as shown below:

function processEntry(entry) {
    if (entry.intersectionRatio === 1) {
        // your code
    }
}

Similarly, if you want to know when a given element is visible by more than half of its height, that also ain't a difficult task. Just alter the conditional expression above to meet your needs.

function processEntry(entry) {
    if (entry.intersectionRatio >= 0.5) {
        // your code
    }
}

In simple words, whenever one wants a target to meet a given intersection ratio before being processed, we can just put a check for the ratio.

However, if we want to know when a given target is merely touching the edges of the root, that's totally impossible to do using intersectionRatio.

Consider the following code:

function processEntry(entry) {
    if (entry.intersectionRatio === 0) {
        console.log("OK");
    }
}

The log here will be made in two cases: if the target is touching the edges of the viewport (which is what we want) and even when it is not touching its edges (which we don't want).

This is because in both these cases the intersectionRatio of the target is equal to 0.

For example, if we want to lazy load an image as soon as its about to enter the viewport i.e is merely touching the edges of the viewport, using the check above would mean loading even those images that are not even close to the edges of the viewport; rather far far below the fold.

To mitigate this problem, the IntersectionObserverEntry interface provides the isIntersecting property.

isIntersecting is a Boolean value that essentially serves the following purpose.

For a threshold that includes the value 0, if isIntersecting is true it means that the target is either touching the edges of or intersecting with the root.

Likewise,

For a threshold that includes the value 0, if isIntersecting is false, it means that the target is neither intersecting, nor touching the edges of the root.
There's a reason why we've said 'For a threshold that includes the value 0' which will be discussed below.

Let's see an example of where to possibly use isIntersecting.

We have a div element, as before, being observed for the default threshold, relative to the default root. (Do you remember the default?)

The idea is to make a log "OK" once the div is touching the edges of the root or actually intersecting with it.

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

function callback(entries) {
    if (entries[0].isIntersecting) {
        console.log("OK");
    }
}

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

In the link below, we have provided a button to precisely bring the div to a point that it touches the bottom edge of the viewport.

Live Example

As soon as you press it, you get a log saying "OK", which means isIntersecting is true. But remember, you still won't be able to see the div; after all it is just touching the viewport's edges, not intersecting with true.

Thing to note!

One extremely important thing to note here is that isIntersecting shall only be used for the purpose it has been designed to serve.

That is, use isIntersecting only if you need to check whether a target is completely away from the root (not even touching its edges).

If, for instance, you need to check whether a target is completely visible in the root, it's much safer to check intersectionRatio than to check isIntersecting.

But why?

Simply because the behavior of isIntersecting is incompatible across various modern browsers.

Yup! Modern browsers - forget about the older ones!

Let's discuss on some....

In Microsoft Edge, if isIntersecting is true it means that the target is visible in the root, or touching with its edges. The threshold property isn't considered while evaluating this value - rather it depends on the target's actual visibility in the root.

Similarly, in Firefox, if isIntersecting is true it means that the target is visible in the root, or touching its edges. The threshold isn't considered.

In Chrome and Opera however, if isIntersecting is true it means that at least one of given threshold values is currently met. That is, the threshold is considered.

Now, if your desirable threshold is [1] and you want to perform some action when the target is completely visible in the root, the following code will produce some very unexpected results:

function processEntry(entry) {
    if (entry.isIntersecting) {
        // perform your desired actions
    }
}
Here, we're assuming that the function processEntry() is invoked from within the callback of the observer and is passed the first element of the entries array.

In Chrome and Opera, this code will put a log when the target is completely visible in the root i.e intersectionRatio is equal to the value 1; and that is exactly what we desire.

Unfortunately, in Edge and Firefox, there's no such guarantee - the log might be made when the target is only touching the edges of the root i.e intersectionRatio is equal to 0; and this is obviously not what we desire.

Therefore the boil down from this fairly long discussion is that, we should only use isIntersecting for a threshold list that includes 0 to normalise the strange behavior of all browsers with this Boolean property.

Other than that, if our threshold doesn't include 0, we're better off at directly checking intersectionRatio, than checking isIntersecting.

Remember this!

Getting the target

So you're going through all the entries in the callback first argument and want to get the target element of each object. Well it ain't difficult - just use target.

The target property of an IntersectionObserverEntry object simply holds a reference to its corresponding target element.

Almost everytime we are working with intersection observers, we ought to perform functions on the target element, such as make it fade-in, or load the underlying image (in the case of lazy loading an img).

Likewise, target is a pretty useful deal!

In the following example, we'll use a snippet from our lazy loading tutorial, to demonstrate a practical-level usage of the property.

The idea is that as soon as an image is about to enter the viewport, we load it by setting a src attribute on it.

For more details on how lazy loading works, or how to make this simple program more complex by introducing transitions, loading icons and more, please consider reading

'About to enter the viewport' means that threshold will be [0] and root will be null. Shown below is the code to lazily load a single image:

<img class="lazy_img" data-src="someImage.jpg">
.lazy_img {
    /* make it easy for us to see where a lazy image is */
    background-color: #ddd;
    padding: 15px;
    width: 80%;

    /* make sure it's far away from the viewport */
    margin: 1000px 0;
}
var image = document.getElementsByClassName("lazy_img")[0];

function callback(entries) {
    entry = entries[0];
    if (entry.isIntersecting) {
        entry.target.src = entry.target.getAttribute("data-src");
    }
}

var io = new IntersectionObserver(callback);
io.observe(image);

Live Example

This code lazy loads only one image. For multiple images consider the task below.
Here we've omitted the options argument to the IntersectionObserver() constructor because we actually want its defaults: root is null and threshold is [0].

The check in line 5 ensures that the image is actually touching the edges of the viewport, before we load it.

This is a MUST to have, otherwise as soon as the page loads, and the target shows up in entries, we'd be loading it; even though it's below-the-fold.

To understand what's meant by below-the-fold please read Difference between above- and below-the-fold.

Extending from the example above, construct a program to observe all .lazy_img elements and load them as soon as they are about to enter the viewport.

To iterate over the entries array you shall use the forEach() method.

var images = document.getElementsByClassName("lazy_img");

function callback(entries) {
    entries.forEach(function(entry) {
        if (entry.isIntersecting) {
            entry.target.src = entry.target.getAttribute("data-src");
        }
    });
}

var io = new IntersectionObserver(callback);

for (var image of images) {
    io.observe(image);
}

Live Example

What time is it?

The time property is pretty apparent on what purpose it serves - keep track of the time!

But unlike it apparency, it's usage percentage is not amongst the top scorers. The time property is used quite rarely, but has some applications.

Anyways, time holds a number to represent the time elapsed since the page load, in units of milliseconds, when the corresponding target was put in the entries array.

Let's see a quick example.

Suppose we want the log the time elapsed by a target while it stays in the viewport. What we'll need to do in order to accomplish this is to save the time at which the target appears completely in the viewport and then subtract this from the time at which the target goes completely out of the viewport.

Take note of the word 'completely' here!

The result of the subtraction is the time the target stayed in the viewport. So simple!

To elaborate on the algorithm:

We start by checking whether the div is completely in the viewport. If it is completely in the viewport, we save the time property at this point inside a global variable appearanceTime.

However, if this is not the case, then we ought to perform further checks to confirm the case in which we are.

Firstly, if apperanceTime is null it simply means that the div hasn't yet shown up completely in the viewport and therefore we must exit right away. There's just no need to move on!

However, if appearanceTime is not null, then it means that the div has indeed shown up previously in the viewport and now we must confirm whether we are completely out of view in order to calculate the time duration.

Shown below is the code to accomplish all this:

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

var appearanceTime = null;

function callback(entries) {
    entries.forEach(function (entry) {
        // reliably check if div is completely visible in the viewport
        if (entry.intersectionRatio === 1) {
            // record the time
            appearanceTime = entry.time;
        }
        // if it's not completely visible, we perform further checks
        else {
            // if appearanceTime is null, exit right away
            if (appearanceTime === null) return;

            // otherwise confirm whether div is completely out of view
            else if (entry.intersectionRatio === 0) {
                // if YES, then calculate the time duration
                appearanceTime = (entry.time - appearanceTime) / 1000;
                console.log(appearanceTime + " seconds in view")
            }
        }
    });
}

var io = new IntersectionObserver(callback, { threshold: [0, 1] });
io.observe(target);

Live Example

If you notice, there is a slight problem in the code above and a very apparent solution to it, at the same time.

The problem is that the code just calculates and logs each appearance/disapperance time; not the whole time a target appears in view as a single value. The solution to this is left as a task for you to do!

Rewrite the code above creating a variable totalTime that holds the total time the div has stayed in the viewport and logs this in place of appearanceTime.

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

var appearanceTime = null,
     totalTime = 0;

function callback(entries) {
    entries.forEach(function (entry) {
        if (entry.intersectionRatio === 1) {
            appearanceTime = entry.time;
        }
        else {
            if (appearanceTime === null) return;
            else if (entry.intersectionRatio === 0) {
                appearanceTime = (entry.time - appearanceTime) / 1000;
                totalTime += appearanceTime;
                console.log(totalTime + " seconds in view")
            }
        }
    });
}

var io = new IntersectionObserver(callback, { threshold: [0, 1] });
io.observe(target);

Live Example

Working with dimensions

Besides all these useful properties, the IntersectionObserverEntry interface also provides three bounding boxes to developers to easily work with dimensions.

They are boundingRect, intersectionRect and rootBounds.

boundingRect is the bounding box of the target, rootBounds is the bounding box of the root, and intersectionRect is the bounding box of the region where the target intersects with the root.

As such, there isn't really any significant usage of these properties, as compared to intersectionRatio, or target.

Regardless, they're provided due to the fact that the calculations made by the IntersectionObserver API in the background, while observing targets, utilise these bounding boxes and so it isn't any difficult to provide them to each respective entry object.

Talking about the application of these properties, we'll be demonstrating the property intersectionRect below.

<div>A div</div>
div {
    padding: 150px;
    margin: 1000px 0;
    display: inline-block;
    background-color: beige;
}

As always, we are observing a div element for which we'll make a log "OK" once it appears in the viewport by 100px, or more than 100px.

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

function thresholdGenerator(start, end, step) {
    var t = [];
    while (start <= end) {
        t.push(start);
        start += step;
    }
    return t;
}

function callback(entries) {
    if (entries[0].intersectionRect.height >= 100) {
        console.log("OK");
    }
}

var options = {
    threshold: thresholdGenerator(0, 1, 0.1)
}

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

The reason of using thresholdGenerator() to create the observer's threshold list is to ensure that the callback potentially fires for a number of ratios, where one ratio might meet the 100px check.

A threshold such as [0, 0.5, 1] will also work, but with this one the callback might fire when the div is way beyond 100px. A finer threshold list will ensure that we are very closely checking for that 100px mark.

If you want even more precise observation for the 100px mark, consider reducing the step argument to thresholdGenerator() from 0.1 to 0.01.

The height of intersectionRect is what we are concerned with; likewise, it goes in the conditional expression in line 13.

Live Example

Although this code executes without any issues, there's is yet a problem in the way it makes the log. Consider the task below to understand the problem.

The code above makes console logs each time the callback fires and intersectionRect.height is greater than or equal to 100.

Your task is to rewrite it in such a way that it makes a log only once, when the div appears in the viewport by 100px or more than 100px, for the very first time.

Unobserve div after the first time it appears.

The solution to this task is very straightforward.

Inside the conditional block, where we make the log, we just need to add the statement io.unobserve(target).

function callback(entries) {
    if (entries[0].intersectionRect.height >= 100) {
        this.unobserve(target);
        console.log("OK");
    }
}

// everything else remains the same