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.
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.
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);
}
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.
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:
intersectionRatio
- a number between 0 and 1 (both inclusive) to indicate the ratio of the intersection between the target and the root.isIntersecting
- a Boolean value to indicate whetherintersectionRatio
is greater than (or equal to) a threshold value or lesser than one.target
- the current target element.time
- a number representing the time (in milliseconds) since the page load, at which thetarget
cut past a threshold value.boundingRect
- the bounding box of the target (excluding any scrollbars).intersectionRect
- the bounding box of the intersection rectangle (excluding any scrollbars).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.
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);
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.
0
, if isIntersecting
is true
it means that the target is either touching the edges of or intersecting with the root.Likewise,
0
, if isIntersecting
is false
, it means that the target is neither intersecting, nor touching the edges of the root.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.
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?
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
}
}
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.
'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);
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.
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);
}
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.
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);
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);
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.
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.
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