The Intersection Observer API provides a way to asynchronously observe changes in the intersection (visibility) of a target element with an ancestor element or the top-level document’s viewport. It is commonly used in implementing infinite scroll, triggering animations when an element enters the viewport, lazy-loading images etc.
It simplifies the process of triggering actions when elements become visible, eliminating the need for manual event handling and frequent polling.
In this article, we will implement an Infinite Scrolling using Intersection Observer API in vanilla Javascript.Infinite scrolling
is an interaction design pattern in which a page loads content as the user scrolls down, allowing the user to explore a large amount of content with no distinct end.( I'm going to demonstrate how to use IntersectionObserver to implement infinite scrolling by fetching new content when a target element becomes visible (within our browser window))
We want the moment target element in our case Loading More Items
as shown below becomes fully visible 100% in our browser window, then we fetch posts, think of it as essentially "sensing" or detecting when the user has scrolled to the bottom of the current content. This detection is facilitated by the IntersectionObserver API
, which observes a target element at the bottom of the content (in our case). When this target element intersects with the viewport (i.e., comes into view, becomes fully visible like in the picture below), it triggers a callback function to load more content ( fetch more posts )
Lets first famirialize ourselves with Intersection Observer API
1. Create a IntersectionObserver - this observer
will watch elements for us
let observer = new IntersectionObserver(callbackFunc, options)
callbackFunc
- this callback will be called when the target element intersects with the device's viewport or the specified element
In other words, when the target element becomes visible or when it's visibility changes, this function will be called. when we talk of the target element "intersecting with the viewport" we mean when the part or all of that element becomes visible within the user's browser window.
options
- let's you control the circumstances under which the observer's callback callbackFunc
is invoked by defining a threshold. It's like setting instruction rules to your observer
to keep watch of your target element until maybe the target element becomes fully visible, half visible or even an inch visible, then the observer reports to you by basically calling the callbackFunc
. You can afterwards do something either with that target element etc
const options = { root: null, threshold: 0.5 };
threshold: 0.5
we say the callback will triggered when 50% of the target element is visible.
Therefore The observer’s callback function
gets triggered when the intersection conditions (defined by the threshold
) are met.
By default root
is null, here we are passing it explicitly, but failure to include the property root
it will be implicitly be set to null... NB you can set the root to be any element
2. Observer - we use observe
method to start monitoring / observing a target element.You can have one or more target elements
observer.observe(targetElement);
observer
: An instance of IntersectionObserver
.
targetElement
: The DOM element you want to observe for visibility changes.
The observe
method is used to start observing a specific target element. When called, it tells the IntersectionObserver
instance to track the specified element and report back whenever it becomes visible or invisible within the viewport or a defined root element.
Lets Wire Everything Up with a Clear Example
We will implement a simple web app that loads Posts dynamically as the user scrolls
- Define a Simple Html Structure
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Infinite Scroll Example</title>
<style>
#content {
max-width: 600px;
margin: 0 auto;
}
.item {
padding: 20px;
border: 1px solid #ccc;
margin: 10px 0;
}
</style>
</head>
<body>
<div id="content">
<!-- Posts will be dynamically loaded here -->
</div>
<!-- This will be our target element -->
<div id="loading" style="text-align: center; margin: 20px; background-color: rgb(173, 160, 185);">
Loading more items...
</div>
<script src="InfiniteScroll.js"></script>
</body>
</html>
- Create a Javascrit file and call it InfiniteScroll.js
Lets initialize some Important Variables here at the top
please take note of the inline documentation for explanation
let currentPage = 1; // this will help us keep track of the current page of data
let loading = false; // this will prevent multiple fetches of data at the same time
const contentDiv = document.getElementById('content'); // our post will be dynamically loaded here
const loadingDiv = document.getElementById('loading'); // this will be our target element
We are going to define our function that is going to be responsible of fetching the post
We will use https://jsonplaceholder.typicode.com/posts?_limit=10&_page=${page}
API endpoint.
Lets break it down, we have the base URL https://jsonplaceholder.typicode.com/posts
this points to the 'post' resource on the JSONPlaceholder API and it provides data related to posts.
We have also have query parameters ?_limit=10&_page=${page}
which filters the request
_limit=10
this tells the API to limit the response to 10 posts
_page=${page}
this specifies which page of data you want to retrieve
For example, _page=1
would get the first set of 10 posts, _page=2
would get the next 10 posts, and so on. we will change the page variable dynamically
hence our function will look like this
const getPosts = async (page) => {
try {
let response = await fetch(`https://jsonplaceholder.typicode.com/posts?_limit=10&_page=${page}`);
if (!response.ok) {
throw new Error("HTTP error! Status: " + response.status);
}
return await response.json();
} catch (e) {
throw new Error("Failed to fetch services: " + e.message);
}
}
- Function that is going to append the data dynamically in our web page. we are going to keep it simple
const appendData = (data) => {
data.forEach(item => {
const div = document.createElement('div');
div.className = 'item';
div.innerHTML = `<h3>${item.title}</h3><p>${item.body}</p>`;
contentDiv.appendChild(div);
});
}
Drum roll please...Now Here is Where the magic starts
- Setting up the intersection observer
const observer = new IntersectionObserver(async (entries) => {
if (entries[0].isIntersecting && !loading) {
console.log(entries)
loading = true;
currentPage++;
try {
const data = await getPosts(currentPage);
appendData(data);
} catch (e) {
console.log(e.message);
}
loading = false;
}
}, { threshold: 1.0 });
Lets go through the above code step by step
First we are a creating a instance called observer
const observer = new IntersectionObserver(async (entries) => {}, { threshold: 1.0 });
We are passing two arguments callbackFunc
and the options
.
If you only pass the threshold option
like { threshold: 1.0 }
when creating an IntersectionObserver, it means that the root
is implicitly set to its default value, which is null
. In this case, the observer will use the browser's viewport as the root
for detecting visibility. 1.0
means when the target is 100% visible, call the callbackFUnc
immeadiatly.
The callback takes 2 arguments, the entries
and the observer
which is the InstersectionObserver itself. In our case we are passing only the entries
argument.
What is this entries
??, this is An array of IntersectionObserverEntry
objects. Each entry represents an observed element / target element and contains information about its intersection with the root / or any specified element.
Let us first start observing our target element then we go back to the above explanation, it will make sense
observer.observe(loadingDiv);
Here we are using observe
method which when called, it tells the IntersectionObserver
instance to track the specified element and report back whenever it becomes visible 100%
or 1.0
as we set the threshold
to that.
When the visibility of the target element changes according to the specified threshold, the callback function
is triggered with an array of IntersectionObserverEntry
objects and we are receiving them as entries, which provide details about the intersection.
Going back to the callbackfunction above, when we console.log(entries)
we have something as below. You can clearly see that the entry represents an observed element (target element) and contains information about its intersection with the root.Since we are observing one target element, our entries array contains only one IntersectionObserverEntry
object.
- We have
time
: This is Timestamp indicating when the intersection occurred / when the target element became fully visible in the browser window - We have
target
: This is The observed element itself. Note that now you have freedom to manipulate this element like chaning its color etc 3.boundingClientRect
: Position and size of the observed element -
intersectionRect:
Intersection area relative to the root -
rootBounds:
Size of the root (viewport) / in our case the broswer window -
isIntersecting
: A boolean indicating whether the observed element is currently intersecting (whether it is visible) with the root (or viewport) according to the specified threshold
Then inside the callback function, this is what we are doing;
(entries[0].isIntersecting && !loading)
: Checking if the first entry (entries[0])
is intersecting with the viewport (isIntersecting is true
) and ensures that loading
isfalse
to prevent multiple concurrent posts fetches.
loading = true:
we are Seting the loading
flag to true
to indicate that a data fetch is in progress.
currentPage++:
Is Incrementing the currentPage
variable to fetch the next page of data.
const data = await getServices(currentPage)
: we are calling the asynchronous getPosts function to fetch Posts for the incremented currentPage.
appendData(data)
: Appends the fetched posts (data) to the DOM using the appendData
function.
loading = false
: we are reseting the loading flag to false
after posts fetching and appending are completed. This allows subsequent fetches to occur when the loadingDiv intersects (becomes fully visible) with the viewport again.
Lets monitor the progress of our infinite scroll implementation and see how requests are being made using the Developer Tools in our browser
Finally we have Window Event Listener:
window.addEventListener('DOMContentLoaded', async () => {
try {
const posts = await getPosts(currentPage);
if (posts) {
appendData(posts);
} else {
console.log('posts not found or undefined');
}
} catch (e) {
console.log(e.message);
}
});
Executes when the DOM content is fully loaded. Calls getPosts to fetch initial posts (currentPage = 1
) and appends it using appendData
Full Code
let currentPage = 1; // this will help us keep track of the current page of data
let loading = false; // this will prevent multiple fetches of data at the same time
const contentDiv = document.getElementById('content'); // our post will be dynamically loaded here
const loadingDiv = document.getElementById('loading'); // this will be our target element
const getPosts = async (page) => {
try {
let response = await fetch(`https://jsonplaceholder.typicode.com/posts?_limit=10&_page=${page}`);
if (!response.ok) {
throw new Error("HTTP error! Status: " + response.status);
}
return await response.json();
} catch (e) {
throw new Error("Failed to fetch services: " + e.message);
}
}
const appendData = (data) => {
data.forEach(item => {
const div = document.createElement('div');
div.className = 'item';
div.innerHTML = `<h3>${item.title}</h3><p>${item.body}</p>`;
contentDiv.appendChild(div);
});
}
const observer = new IntersectionObserver(async (entries) => {
if (entries[0].isIntersecting && !loading) {
console.log(entries)
loading = true;
currentPage++;
try {
const data = await getPosts(currentPage);
appendData(data);
} catch (e) {
console.log(e.message);
}
loading = false;
}
}, { threshold: 1.0 });
observer.observe(loadingDiv);
window.addEventListener('DOMContentLoaded', async () => {
try {
const posts = await getPosts(currentPage);
if (posts) {
appendData(posts);
} else {
console.log('posts not found or undefined');
}
} catch (e) {
console.log(e.message);
}
});
Final Thoughts
In this article, we've explored the powerful IntersectionObserver API and its role in creating seamless infinite scrolling experiences. By leveraging this API, you can enhance the usability of your web applications, making them more dynamic and engaging for users. Here's a recap of what we covered
Key Concepts:
IntersectionObserver API:
Provides an efficient way to monitor the visibility of target elements within the viewport.
Simplifies the process of triggering actions when elements become visible, eliminating the need for manual event handling and frequent polling.
Infinite Scrolling:
Allows for continuous content loading as the user scrolls, improving the browsing experience by providing a steady stream of new content without requiring page reloads or manual pagination.
Callback Functions and Options:
The callbackFunc
is called when the target element's visibility changes based on specified threshold values.
The options
object allows fine-tuning of the observer's behavior, such as setting the root and threshold.
Conclusion
By mastering the IntersectionObserver API, you can create sophisticated and user-friendly web applications that offer a seamless and interactive experience. Whether you're building a news feed, an e-commerce site, or any other content-heavy application, infinite scrolling powered by IntersectionObserver ensures that users stay engaged and your app performs efficiently.
So go ahead, dive into your code editor, and start implementing infinite scrolling in your projects today. Your users will thank you for the smooth and continuous browsing experience!
Top comments (4)
Good post. The
rootMargin
option is a good one to know. It's useful if you want to start loading the next batch of results a bit optimistically before the end of the current content is in view, which works if you supply a negative vertical value (e.g. to start loading when the end is300px
from the bottom of the window).Good Point Sir,,It's also worth noting that failure to include it, it defaults to
0px 0px 0px 0px
Interesting post! The intersection observer is a great browser api in JavaScript.
sure bro,,Js will remain to be king of the Web,,look forwad for posts like this and many more