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.
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,
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:
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.
Can you guess how it's calculated?
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.
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.
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.
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.
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 tonull
, which means that the root is the viewport.rootMargin
: A string to indicate the margins around the root, similar to the CSSmargin
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.
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 statementconsole.log("OK")
in it. In this case we use a named functioncallback()
.root
will be the valuenull
because we are concerned with the viewport (which is actually redundant to mention sincenull
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.
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.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:
- When is the callback exactly fired.
- 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.).
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.
In another instance:
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.
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.
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.
As soon as you open the document and its console, you'll rightaway 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.
Thing to note!
One crucial thing to remember in this regards is that:
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.
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.
- The first
entries
argument is an array ofIntersectionObserverEntry
objects. We'll explore this in the section below. - The second
observer
argument is simply a reference to the observer object (available viathis
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>
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);
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.
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!
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:
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).
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;
}
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);
[0, 1]
here and not [1]
will be discussed in the next chapter.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!