Intersection Observer Web API
How to detect a DOM element's visibility in relation to other elements or the viewport
Published: 16 February 2020
Basics
Intersection Observer API is part of Web APIs which helps in finding a DOM element's visibility on the screen.
This has many use cases but we will look at the 2 most common ones - infinite scrolling and sticky headers.
Setting up this API is a 2 step process:
// index.js
const observer = new IntersectionObserver(callbackFunction, {
root: document.getElementById("#parent"),
threshold: 0.5,
})
const targetElement = document.getElementById("#some-element")
observer.observe(targetElement)
Lets's break the components down:
CallbackFunction --> gets fired under 2 conditions:
It receives an array of IntersectionObserverEntry object/s as first argument, with each object giving information about the target elements being observed. If only one element is being observed, then this array will only have one object. More on this later. Its second argument is the observer itself.
Root --> is the element that intersects with a given element (it can be a parent element or simply the viewport).
TargetElement --> is the element whose visibility we want to inquire about in relation to the root.
Threshold --> defines how much of the target element should be viewable upon intersection with the root.
It ranges from 0-1, where 0 means none of the element is showing and 1 means the whole element has to be visible before the condition is considered as having met.
It could also be an array of values ranging from 0-1 like so - [0, 0.25, 0.75, 1]. This will indicate that callback function should run each time those percentages of target element become visible.
Now let's try and achieve the following:
Infinite Scrolling
NOTE: Although, we have used Vue in the repo example, this API can be just as easily in Vanilla JS or any other frameworks.
For the examples below, we will show simple HTML and JS.
Let's assume we have a list of people and we would like to display their emails. However, we will only show the first 6 on initial load. Once we scroll to the 6th or the last email, we would like to fetch 6 more.
Conceptually, we are trying to target the last element in the list of people. As soon as we even see a pixel of that target, we will trigger an AJAX call to get the next 6 people.
Sample HTML:
<style>
.person {
padding: 100px;
}
</style>
<div id="container">
<p class="person">First Email</p>
<p class="person">Second Email</p>
<p class="person">Third Email</p>
<p class="person">Fourth Email</p>
<p class="person">Fifth Email</p>
<p class="person">Sixth Email</p>
<a>I am only here to create an error which will be explained later</a>
</div>
Sample Javascript:
const observer = new IntersectionObserver(callbackFunc, {
root: null, //targeting the viewport itself
threshold: 0.5, //when half the element is visible, run the callbackFunc
})
observer.observe(document.querySelector(".person:last-child"))
function callbackFunc(entries, observer) {
console.log("The element you are targeting is now visible or is it?")
//AJAX call here
}
NOTE: document.querySelector(".person:last-child") won't return anything and so observer will throw an error.
This is because we have an anchor tag at the end of the container which will be treated as the last child. So, we must either take it out or wrap our paragraphs in their own separate div like so:
<div id="container">
<div class="persons-container">
<p class="person">First Email</p>
<p class="person">Second Email</p>
<p class="person">Third Email</p>
<p class="person">Fourth Email</p>
<p class="person">Fifth Email</p>
<p class="person">Sixth Email</p>
</div>
<a>I am only here to create an error scenario</a>
</div>
Callback Function - Most important Piece
The callback function shown above may not create ideal effects, so let's look at why.
Firstly, it will run once as soon as the observer is setup on the target element at which point our element may or may not be visible. If we decide to make an HTTP call at that point, we will end up making it ahead of time and that may not be what we want.
To mitigate this, lets tap into its first argument - entries which is an array of objects but in our case only a single object since we are only targeting a single DOM element.
It has the following properties:
function callbackFunc(entries, observer) {
entries = [
{
boundingClientRect,
intersectionRatio, //how much of the target element is intersecting with the root
intersectionRect, // dimensions of the intersection
isIntersecting, // IMPORTANT --> true when target element has intersected, false otherwise
rootBounds,
target,
time,
},
]
}
We can use isIntersecting prop to figure out when exactly our target element becomes visible.
function callbackFunc(entries, observer) {
if (entries[0].isIntersecting) {
console.log("The element you are targeting is definitely visible")
//make AJAX call now to fetch 6 more people
}
}
Sticky Header
Just like infinite scrolling, our goal is to figure out at what point do we show our sticky header.
We know that main header will be shown upon load.
So, as soon as main header stops showing, we would show sticky header. To achieve this, we can set our threshold to 0. That will indicate, we want to run our callbackFunc when 0% of the main header is showing.
Sample HTML
<div id="container">
<h1 class="main-header">Main Fat header</h1>
<h3 class="sticky-header">Smaller sticky header</h3>
<p class="person">First Email</p>
<p class="person">Second Email</p>
<p class="person">Third Email</p>
</div>
<script>
//hide the sticky header to begin with
document.querySelector(".sticky-header").style.display = "none"
//create new observer instance
const stickyObserver = new IntersectionObserver(callbackFunc, {
root: null, //targeting the viewport itself
threshold: 0, //when none of the element is visible, run the callbackFunc
})
//start watching main header
stickyObserver.observe(document.querySelector(".main-header"))
function callbackFunc(entries, observer) {
if (entries[0].isIntersecting) {
// main header is visible and so threshold is at 1, do nothing
} else {
// show sticky header
document.querySelector(".sticky-header").style.display = "block"
}
}
</script>
Removing an observer
Always make sure to remove to observer once you are done with a component to avoid unnecessary performance hit.
stickyObserver.disconnect()