Intersection Observer API in Javascript

Is the Element Actually Visible in the Screen ?

Many times you will like to know whether a target element in the webpage is visible in the screen or not. Furthermore you would like to do something once the element becomes visible in the screen or gets hidden. Some examples can be :

  • Lazy loading of images as user scrolls the page
  • Run an animation when user reaches a certain section of the page
  • Implement infinite scrolling

Until now, developers have managed it, somehow. The confidence that code will run correctly in all situations, and in all devices, was low.

But now Intersection Observer API has arrived. It has taken care of all such situations, and probably it is one of the most useful Web APIs that have been released recently.

Adding to the good news, all current browsers have added support it — Chrome, Firefox, Edge & Safari. For Internet Explorers and other older browsers, you can use a polyfill without making any separate changes to the code.

Browser viewport and screen are used interchangeably in this tutorial — both refer to the visible part of the browser that holds a webpage.

Prior to Intersection Observer API

It will be noteworthy to give a brief description of how visibility of an element in the screen was handled before Intersection Observer API came along.

As an example consider the case of infinite scrolling. To handle infinite scrolling, you will need to place an element (a "Show More" button or an empty <div>) at the bottom of the page, and as the user scrolls the page and reaches that element, more content is appended to the page. So the main issue is to keep track when the "Show More" element becomes visible on the screen, so that more content can be loaded.

In Javascript code a handler is attached to the scroll event. In the event handler the position of the element relative to the browser viewport is found using getBoundingClientRect(). Every time the scroll event occurs, the position of the element is calculated.

The top and bottom properties returned by getBoundingClientRect, along with the height of the screen gives an idea about whether element is visible on the screen or not.

Below are the codes to detect if the element is fully visible on the screen. With a slight tweak to the codes, you can also check for partial visibility.

window.addEventListener('scroll', function() {
	var element = document.querySelector('#target-container');
	var position = element.getBoundingClientRect();

	// detecting if element is fully visible
	if(position.top >= 0 && position.bottom <= window.innerHeight) {
		// fully visible
	}
});

In addition to handling mathematical calculations in the code (which are prone to bugs), the method also had a lot of performance issues :

  1. scroll events fire at a high rate, so if the callback is doing heavy operations (which is being done in the current case), the performance of the application will degrade
  2. getBoundingClientRect method will force a re-layout of the DOM — the exact position and size of each element in page is re-calculated, then they are re-shown on the screen
  3. Detecting for scroll is being done on the main thread — there is no background process that checks whether window is being scrolled
  4. For mobiles, the scroll callback is executed only after user has finished scrolling

This was the reason why Intersection Observer API had to come up.

All Elements in a Webpage are Rectangles

Before starting the explanation of the API, you must know that all HTML elements are painted as rectanges on the screen. Even when an element looks curved or circular, it is essentially a rectangle internally.

The browser viewport is also a rectangle.

Intersection Between Rectangles

When 2 rectangles are placed together they may intersect (or overlap).

The below images shows multiple elements in a webpage and how they intersect with the screen.

Introducing Intersection Observer API

The Intersection Observer API keeps track of intersection between 2 elements in the page. The 2 elements are the target element and root element.

  1. The target element — this is the element you are mostly interested in.
  2. The root element — you are looking to know how much area of the target element is overlapped by the root element.

In Javascript code you define the target and root element. The root element can either be the browser viewport or a parent of the target element :

  1. If you don't set the root element or set it to null, the browser viewport (or the screen) is set as the root element. That is — you are looking to find intersection between the element and the screen.
    Obviously, if the screen intersects the element, it means element is visible in the screen. If the screen intersects 50% of the element, it means 50% of element is seen on screen. And if there no intersection between element and screen, it means element is not visible in screen.
  2. Root element can also be a parent of the target element. In this case the intersection between the child and parent element will be observed.
    For example, consider a scrollable <div> in the page, and a button in it. Depending on the amount of scroll, the button may or may not be visible. With the Intersection Observer API, you can find when the button gets visible/hidden as the parent is scrolled. You can also get how much area of the button is visible in the parent.

To use the Intersection Observer API you will need to create a new IntersectionObserver object. The root element can be set with the root option property. To set the screen as the root element, you can set root to null, or leave it out.

To start observing the target for intersection, you use the observe method.

// new object with screen as root element
var observer = new IntersectionObserver(function() {
	// callback code
}, { root: null });

// observing a target element
observer.observe(document.querySelector("#target-container"));
// root is a parent of the target element
var observer = new IntersectionObserver(function() {
	// callback code
}, { root: document.querySelector('#parent-container') });

// observing a target element
observer.observe(document.querySelector("#target-container"));

Intersection Observer Fires a Callback on Crossing a threshold

  • The Intersection Observer API fires a callback when the visibility of the target element crosses a specified threshold. A threshold is a number between 0 and 1 that represents how much percent of the target element is intersected by the root. A value of 0 means 0% intersection and value of 1 means 100% intersection.

    For example, you set the threshold as 0.5. Intersection Observer will fire a callback when the intersection area of target element crosses 50% in either direction — callback will be fired every time when viewable area passes the 50% benchmark upwards (for example 30% to 55%), or downwards (for example 60% to 0%).

  • Note that callback is executed once intersection reaches somewhere around the given threshold — obviously it is not possible for the browser to keep track of an EXACT intersection value. For example if you set threshold as 0.5, callback may be executed when intersection becomes 0.54

  • All threshold values are given in threshold option while creating a new IntersectionObserver object. It is specified as an array.

    // root element is default : screen
    var observer = new IntersectionObserver(function() {
    	// callback code
    }, { threshold: [0.5] });
    
    observer.observe(document.querySelector("#target-container"));
    
  • The default value of threshold is [0]. This means that if you don't set the threshold option, callback will be fired once target element becomes partially visible in root (by few pixels) or completely leaves the root element.

    // threshold option is default : [0]
    // root element is default : screen
    var observer = new IntersectionObserver(function() {
    	// callback code
    });
    
  • It is also possible to set multiple thresholds if you are looking to observe the target element on various percentages of intersection. Allowing multiple thresholds to be set is the reason why threshold is specified as an array.

    // multiple thresholds
    var observer = new IntersectionObserver(function() {
    	// callback code
    }, { root: document.querySelector('#parent-container'), threshold: [0, 0.5, 1] });
    
    observer.observe(document.querySelector("#target-container"));
    

Once again, remember, callback is fired when target crosses a threshold in either direction.

Multiple Targets Can Also be Observed with Same IntersectionObserver Object

It is also possible to observe multiple elements using the same IntersectionObserver object by calling observe for each target.

// root element is default : screen
var observer = new IntersectionObserver(function() {
	// callback code
}, { threshold: [0.5] });

// observing first target element
observer.observe(document.querySelector("#target-container-1"));

// observing second target element
observer.observe(document.querySelector("#target-container-2"));

Note that all targets will have the same threshold and root. If you need targets to have different thresholds or root elements, you will need to create a separate IntersectionObserver object for each of them.

Callback Receives an entries Parameter

The callback that is fired receives an entries parameter. This is array of objects, and each object in it contains intersection data for one of the target elements (remember you can observe multiple targets with the same IntersectionObserver object).

var observer = new IntersectionObserver(function(entries) {
	// a single target is specified so entries.length will be 1
});

observer.observe(document.querySelector("#target-container"));
var observer = new IntersectionObserver(function(entries) {
	// Two targets are observed so entries.length may be 1 or 2
	// entries.length will be 1 if thresholds of one of the targets is crossed
	// entries.length will be 2 if thresholds of both targets are crossed
}, { threshold: [0.1] });

// first target
observer.observe(document.querySelector("#target-container-1"));

// second target
observer.observe(document.querySelector("#target-container-2"));

The most useful properties of the intersection data is intersectionRatio, isIntersecting and target.

var observer = new IntersectionObserver(function(entries) {
	for(let i=0; i<entries.length; i++) {
		// entries[i]['intersectionRatio']
		// entries[i]['isIntersecting']
		// entries[i]['target']
	}
});
  1. isIntersecting represents whether target element and root element are intersecting or not. A boolean true means they intersect, while false mean they don't.
    Like told earlier the callback will be executed every time a threshold is crossed in either direction. In this case you can check the value of isIntersecting to find whether the movement was from intersecting to non-intersecting or from non-intersecting to intersecting.
  2. intersectionRatio is the percentage of the target element intersected by the root element. Again rather as a percentage, it is represented as a value between 0 and 1.
    Value of 0 means 0% area is intersected. A value of 0.15 means 15% area is intersected. A value of 1 means 100% intersection.
  3. target is the target DOM element. For a single target, it does not matter much. But if there are multiple targets that are being observed with the same IntersectionObserver object, this property can be used to check which target is the entry referring to.

Demo

See the example code below - it observes a target on thresholds of [ 0, 0.5, 1 ]. root is not set, so browser viewport becomes the root element.

Here is a live demo.

// root is the browser viewport / screen
var observer = new IntersectionObserver(function(entries) {
	// since there is a single target to be observed, there will be only one entry
	if(entries[0]['isIntersecting'] === true) {
		if(entries[0]['intersectionRatio'] === 1)
			console.log('Target is fully visible in the screen');
		else if(entries[0]['intersectionRatio'] > 0.5)
			console.log('More than 50% of target is visible in the screen');
		else 
			console.log('Less than 50% of target is visible in the screen');
	}
	else {
		console.log('Target is not visible in the screen');
	}
}, { threshold: [0, 0.5, 1] });

observer.observe(document.querySelector("#target-container"));

Changing Boundary of Root Element with rootMargin

The final option that can be set in an IntersectionObserver object is rootMargin. This is similar to CSS margin property and can be specified in the same way, for example 20px 0px 20px 0px.

As told earlier intersection is calculated between the rectangular root element and rectangular target element. The rootMargin option adds offset to each side of the reactangular root element. The final rectangle that is formed by adding those offsets to the root is then used as the bounding box to calculate intersection with the target.

var observer = new IntersectionObserver(function(entries) {
	// callback code
}, { threshold: [0.1], rootMargin: "50px 0 50px 0px" });

The default value of rootMargin is 0px 0px 0px 0px.

What is the use of rootMargin option ? Consider a situation where you like the callback to be executed when the the target element is almost arriving in the screen. Setting threshold as [0] would result in callback execution when few pixels of the target becomes visible. But with rootMargin set, say, 20px 0px 0px 0px, callback will be executed earlier when target is almost on the verge of being visible.

Negative values are also allowed in rootMargin.

Stopping an Observer

After your work has been done, you may probably want to stop observing the target element. To do so you can use disconnect method to stop observing all targets of an IntersectionObserver object.

var observer = new IntersectionObserver(function(entries) {
	// callback code
});

observer.observe(document.querySelector("#target-container"));

observer.disconnect();

In case you want to stop observing some specific targets, while observing others, you can use the unobserve method.

var observer = new IntersectionObserver(function(entries) {
	// callback code
});

observer.observe(document.querySelector("#target-container-1"));
observer.observe(document.querySelector("#target-container-2"));

// stop observing #target-container-1
observer.unobserve(document.querySelector("#target-container-1"));

Performance in Intersection Observer

Intersection Observer API gives a great performance boost because it observes an element asynchronously — observation is not done on the main thread, but rather as a background process.

However the callback is synchronous and executed on the main thread — this means if your callback is performing heavy work, you will not get performance advantage given by Intersection Observer.

According to the specification it is advised to use window.requestIdleCallback to queue your callback code to be executed during browser's idle time. window.requestIdleCallback will run the callback code in the background, but obviously you cannot predict when the code will be executed. This is useful for low priority tasks.

Use an appropriate case as per your requirement.

Wrapping Up

  • All elements in a HTML page are drawn as rectanges on the screen
  • Intersection Observer API observes intersection (or overlapping area) between 2 elements — a target and a root
  • The root can be set to the screen or a parent of the target
  • A callback is executed when intersection area of target in the root crosses a given threshold in either direction
  • Boundaries of the root element can also be increased or decreased by adding a margin to it
  • You can also stop observing a target
  • Intersection Observer observes a target asynchronously, but the callback is executed synchronously along with the main application code

Useful Resources on Intersection Observer