In this article, we’re going to learn how to easily implement infinite scrolling on a webpage using JavaScript.
What is infinite scroll?
Infinite scroll is a web design technique where more content is loaded automatically when the user scrolls down to the end. It removes the need for pagination and can increase the time users spend on our site.
Finished infinite scroll project
Our case study for this article will be a small project that demonstrates essential concepts related to infinite scroll.
Here it is:
HTML structure
Before looking at the JavaScript functionality, let’s check out the HTML markup for the project’s webpage.
HTML
<div id="load-trigger-wrapper">
<div id="image-container"></div>
<div id="load-trigger"></div>
</div>
<div id="bottom-panel">
Images:
<b><span id="image-count"></span>
</b>/
<b><span id="image-total"></span></b>
</div>
The #image-container
div
will contain the grid of images.
The #load-trigger
div
is observed by an Intersection Observer; more images will be loaded when this div
comes within a certain distance of the bottom of the viewport.
The #bottom-panel
div
will contain an indicator of the number of images that have been loaded.
Detect scroll to content end
The detectScroll()
function uses the Intersection Observer API to detect when the #bottom-panel
div
comes within a certain range of the viewport’s bottom. We set a root margin of -30px
, so this range is 30px upwards from the bottom.
JavaScript
const loadTrigger = document.getElementById('load-trigger');
// ...
const observer = detectScroll();
// ...
function detectScroll() {
const observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
// ...
loadMoreImages();
// ...
}
},
// Set "rootMargin" because of #bottom-panel height
{ rootMargin: '-30px' }
);
// Start watching #load-trigger div
observer.observe(loadTrigger);
return observer;
}
The callback passed to and Intersection Observer
fires after the observe()
call, so the images are loaded as the page is loaded.
Display skeleton images
Before the actual images are loaded, we first show a blank skeleton image with a loading animation. We store the image elements in an array variable to update them when their respective images have been loaded.
JavaScript
const imageClass = 'image';
const skeletonImageClass = 'skeleton-image';
// ...
// This function would make requests to an image server
function loadMoreImages() {
const newImageElements = [];
// ...
for (let i = 0; i < amountToLoad; i++) {
const image = document.createElement('div');
// Indicate image load
image.classList.add(imageClass, skeletonImageClass);
// Include image in container
imageContainer.appendChild(image);
// Store in temp array to update with actual image when loaded
newImageElements.push(image);
}
// ...
}
To display each image, we create a div
and add the image
and skeleton-image
classes to it. Here are the CSS definitions for these classes:
CSS
.image,
.skeleton-image {
height: 50vh;
border-radius: 5px;
border: 1px solid #c0c0c0;
/* Three per row, with space for margin */
width: calc((100% / 3) - 24px);
/* Initial color before loading animation */
background-color: #eaeaea;
/* Grid spacing */
margin: 8px;
/* Fit into grid */
display: inline-block;
}
.skeleton-image {
transition: all 200ms ease-in;
/* Contain ::after element with absolute positioning */
position: relative;
/* Prevent overflow from ::after element */
overflow: hidden;
}
.skeleton-image::after {
content: "";
/* Cover .skeleton-image div*/
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
/* Setup for slide-in animation */
transform: translateX(-100%);
/* Loader image */
background-image: linear-gradient(90deg, rgba(255, 255, 255, 0) 0, rgba(255, 255, 255, 0.2) 20%, rgba(255, 255, 255, 0.5) 60%, rgba(255, 255, 255, 0));
/* Continue animation until image load*/
animation: load 1s infinite;
}
@keyframes load {
/* Slide-in animation */
100% {
transform: translateX(100%)
}
}
Update skeleton images
Instead of getting images from a server, we get colors. After all the colors are loaded, we loop through the skeleton images, and for each image, we remove the skeleton-image
class and apply the color.
JavaScript
function loadMoreImages() {
// ...
// Create skeleton images and stored them in "newImageElements" variable
// Simulate delay from network request
setTimeout(() => {
// Colors instead of images
const colors = getColors(amountToLoad);
for (let i = 0; i < colors.length; i++) {
const color = colors[i];
newImageElements[i].classList.remove(skeletonImageClass);
newImageElements[i].style.backgroundColor = color;
}
}, 2000);
// ...
}
The getRandomColor()
function takes a number and returns an array with that number of random colors.
JavaScript
function getColors(count) {
const result = [];
let randUrl = undefined;
while (result.length < count) {
// Prevent duplicate images
while (!randUrl || result.includes(randUrl)) {
randUrl = getRandomColor();
}
result.push(randUrl);
}
return result;
}
getColors()
uses a getRandomColor()
function that returns a random color, as its name says.
JavaScript
function getRandomColor() {
const h = Math.floor(Math.random() * 360);
return `hsl(${h}deg, 90%, 85%)`;
}
Stop infinite scroll
To save resources, we stop observing the load trigger element after all possible content has been loaded.
Let’s say we have 50 images that can be loaded and the loadLimit
is 9
. When the first batch of images is loaded, the amountToLoad
should be 9
, with 9 displayed images. When the fifth batch is to be loaded, the amountToLoad
should still be 9
, with 45 displayed images.
On the sixth batch, there’ll only be 5 images left to load, so the amountToLoad
should now be 5, taking the displayed images to 50. This sixth batch of images will be the final one to be loaded, after which we’ll stop watching the load trigger element, with a call to the unobserve()
method of the Intersection Observer
.
So we use the Math.min()
method to ensure that the amountToLoad
is always correct. amountToLoad
should never be more than the, and never less than the images left to load.
JavaScript
const imageCountText = document.getElementById('image-count');
// ...
let imagesShown = 0;
// ...
function loadMoreImages() {
// ...
const amountToLoad = Math.min(loadLimit, imageLimit - imagesShown);
// Load skeleton images...
// Update skeleton images...
// Update image count
imagesShown += amountToLoad;
imageCountText.innerText = imagesShown;
if (imagesShown === imageLimit) {
observer.unobserve(loadTrigger);
}
}
Optimize performance with throttling
If the user scrolls down rapidly, it’s likely that the Intersection Observer will fire multiple times, causing multiple image loads in a short period of time, creating performance problems.
To prevent this, we can use a timer to limit the number of times multiple image batches can be loaded within a certain time period. This is called throttling.
The throttle()
function accepts a callback and a time period in milliseconds (time
). It will not invoke the callback if the current throttle()
call was made withing time
ms of the last throttle()
call.
JavaScript
let throttleTimer;
// Only one image batch can be loaded within a second
const throttleTime = 1000;
// ...
function throttle(callback, time) {
// Prevent additional calls until timeout elapses
if (throttleTimer) {
console.log('throttling');
return;
}
throttleTimer = true;
setTimeout(() => {
callback();
// Allow additional calls after timeout elapses
throttleTimer = false;
}, time);
}
By calling throttle()
in the Intersection Observer’s callback with a time
of 1000
, we ensure that loadMoreImages()
is never called multiple times within a second.
function detectScroll() {
const observer = new IntersectionObserver(
(entries) => {
// ...
throttle(() => {
loadMoreImages();
}, throttleTime);
}
}
},
// ...
);
// ...
}
Finished infinite scroll project
You can check out the complete source code for this project in CodePen. Here’s an embed:
Conclusion
In the article, we learned the basic elements need to implement infinite scroll functionality using JavaScript. With the Intersection Observer API, we observe a load trigger element and load more content when the element gets within a certain distance of the viewport’s bottom. With these ideas in mind, you should able to easily add infinite scroll to your project, customized according to your unique needs.
Every Crazy Thing JavaScript Does
A captivating guide to the subtle caveats and lesser-known parts of JavaScript.
Nice work.