← Back to all tutorials

How to build a lazy-loading image gallery with Tailwind CSS and JavaScript

#_
Published and written on Sep 20 2024 by Michael Andreuzza

Finally Friday! We’re back with another tutorial on how to build a lazy-loading image gallery with Tailwind CSS and JavaScript.

What is lazy-loading?

Lazy-loading is a performance optimization technique where images or other media are only loaded when they enter the viewport (or are about to). Instead of loading all images on page load, lazy-loading helps reduce initial load times, as only the necessary content is loaded first. This results in faster page performance and an overall smoother user experience, especially on image-heavy websites.

Use cases for lazy-loading images

Lazy-loading is particularly useful in the following scenarios:

  • Image galleries: When displaying a large number of images, lazy-loading ensures that only the images in view are loaded, preventing long load times and conserving bandwidth.

  • Long pages: On content-heavy pages (e.g., blogs or e-commerce product listings), lazy-loading helps keep the page load time down by loading images as the user scrolls.

  • Mobile optimization: For users on slower networks or mobile devices, lazy-loading ensures they only download necessary content, preserving data and improving the browsing experience.

  • Progressive web apps (PWAs): Lazy-loading helps maintain the performance of PWAs, ensuring they remain fast and responsive.

In this tutorial, we’ll walk through how to set up a lazy-loading image gallery using Tailwind CSS for styling and a bit of JavaScript to implement the lazy-loading functionality.

Writting the markup

Id’s

  • gallery is the id of the container element that will hold the images. This is where we’ll append the dynamically generated image elements. Classes
  • grid is a utility class that allows us to create a grid layout for the images.
  • grid-cols-1 sets the number of columns to 1, which will create a single column layout.
  • sm:grid-cols-2 sets the number of columns to 2 for screens with a small width (up to 768px).
  • md:grid-cols-3 sets the number of columns to 3 for screens with a medium width (768px to 1024px).
  • lg:grid-cols-4 sets the number of columns to 4 for screens with a large width (1024px and up).
  • gap-4 adds a 4-pixel gap between the images.
<div
   id="gallery"
   class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
   <!-- Images will be dynamically inserted here -->
</div>

Writing the JavaScript

Creating the image elements

We’ll use the createImageElement function to create the image elements dynamically.

  • const div = document.createElement("div"); creates a new div element. We’ll use this element to wrap the image and the placeholder.
  • const imagesUrls = [.... ]; is an array of image URLs. We’ll use this array to create the image elements.
const gallery = document.getElementById("gallery");
const imageUrls = [
   "https://images.unsplash.com/photo-1543610892-0b1f7e6d8ac1?q=80&w=1856&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
   "https://images.unsplash.com/photo-1516914943479-89db7d9ae7f2?q=80&w=2732&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
   "https://images.unsplash.com/photo-1531384698654-7f6e477ca221?q=80&w=2800&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
   "https://images.unsplash.com/photo-1531901599143-df5010ab9438?q=80&w=2787&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
   "https://images.unsplash.com/photo-1524255684952-d7185b509571?q=80&w=2787&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
   "https://images.unsplash.com/photo-1588175996685-a40693ee1087?q=80&w=2864&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
   "https://images.unsplash.com/photo-1624561172888-ac93c696e10c?q=80&w=2592&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
   "https://images.unsplash.com/photo-1489424731084-a5d8b219a5bb?q=80&w=2787&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
];

The createImageElement function

This function takes two arguments: the URL of the image and the index of the image in the array. It returns a new div element that contains the image and a placeholder element to display while the image is loading.

  • function createImageElement(url, index) {: This function takes two arguments: the URL of the image and the index of the image in the array.
  • const div = document.createElement("div");: Creates a new div element.
  • div.className = "relative overflow-hidden aspect-w-16 aspect-h-9";: Sets the class name of the div element to “relative overflow-hidden aspect-w-16 aspect-h-9”. This class sets the element to be a relative container with overflow hidden and an aspect ratio of 16:9.
  • const img = document.createElement("img");: Creates a new img element.
  • img.className = "lazy-image w-full h-full object-cover transition-opacity duration-300 opacity-0";: Sets the class name of the img element to “lazy-image w-full h-full object-cover transition-opacity duration-300 opacity-0”.
  • img.dataset.src = url;: Sets the src attribute of the img element to the URL of the image.
  • img.alt = Image ${index + 1};: Sets the alt attribute of the img element to “Image 1”, “Image 2”, etc.
  • const placeholder = document.createElement("div");: Creates a new div element.
  • placeholder.className = "absolute inset-0 bg-neutral-200 animate-pulse w-full h-full";: Sets the class name of the placeholder element to “absolute inset-0 bg-neutral-200 animate-pulse w-full h-full”.
  • div.appendChild(placeholder);: Appends the placeholder element to the div element.
  • div.appendChild(img);: Appends the img element to the div element.
  • return div;: Returns the div element.
function createImageElement(url, index) {
   const div = document.createElement("div");
   div.className = "relative overflow-hidden   aspect-w-16 aspect-h-9";
   const img = document.createElement("img");
   img.className =
      "lazy-image w-full h-full object-cover transition-opacity duration-300 opacity-0";
   img.dataset.src = url;
   img.alt = `Image ${index + 1}`;
   const placeholder = document.createElement("div");
   placeholder.className = "absolute inset-0 bg-neutral-200 animate-pulse w-full h-full";
   div.appendChild(placeholder);
   div.appendChild(img);
   return div;
}

The lazyLoad function

This function uses the IntersectionObserver API to observe the img elements with the class lazy-image. When an element with the class lazy-image is intersected with the viewport, the src attribute of the element is set to the value of the dataset.src attribute, and the onload event is triggered.

  • function lazyLoad() {: This function uses the IntersectionObserver API to observe the img elements with the class lazy-image.
  • const images = document.querySelectorAll("img.lazy-image");: Selects all img elements with the class lazy-image.
  • const options = {: Creates an object with the options for the IntersectionObserver.
  • root: null,: Sets the root option to null.
  • rootMargin: "0px",: Sets the rootMargin option to “0px”.
  • threshold: 0.1,: Sets the threshold option to 0.1.
  • const imageObserver = new IntersectionObserver((entries, observer) => {: Creates a new IntersectionObserver with the provided callback function.
  • entries.forEach((entry) => {: Iterates over each entry in the IntersectionObserver.
  • if (entry.isIntersecting) {: Checks if the entry is intersecting.
  • const img = entry.target;: Selects the target element of the entry.
  • img.src = img.dataset.src;: Sets the src attribute of the img element to the value of the dataset.src attribute.
  • img.onload = () => {: Triggers the onload event of the img element.
  • img.classList.remove("opacity-0");: Removes the class opacity-0 from the img element.
  • img.previousElementSibling.remove(); // Remove placeholder: Removes the placeholder element from the img element.
  • observer.unobserve(img);: Unobserves the img element.
function lazyLoad() {
   const images = document.querySelectorAll("img.lazy-image");
   const options = {
      root: null,
      rootMargin: "0px",
      threshold: 0.1,
   };
   const imageObserver = new IntersectionObserver((entries, observer) => {
      entries.forEach((entry) => {
         if (entry.isIntersecting) {
            const img = entry.target;
            img.src = img.dataset.src;
            img.onload = () => {
               img.classList.remove("opacity-0");
               img.previousElementSibling.remove(); // Remove placeholder
            };
            observer.unobserve(img);
         }
      });
   }, options);
   images.forEach((img) => imageObserver.observe(img));
}

Creating and appending the image elements

We’ll create and append the image elements to the gallery using the createImageElement function. We’ll also initialize the lazy loading by calling the lazyLoad function.

  • imageUrls.forEach((url, index) => {: Iterates over each URL in the imageUrls array.
  • const imageElement = createImageElement(url, index);: Calls the createImageElement function with the URL and index as arguments.
  • gallery.appendChild(imageElement);: Appends the imageElement to the gallery element.
  • lazyLoad();: Calls the lazyLoad function.
// Create and append image elements
imageUrls.forEach((url, index) => {
   const imageElement = createImageElement(url, index);
   gallery.appendChild(imageElement);
});
// Initialize lazy loading
lazyLoad();

The full script

const gallery = document.getElementById("gallery");
const imageUrls = [
   "https://images.unsplash.com/photo-1543610892-0b1f7e6d8ac1?q=80&w=1856&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
   "https://images.unsplash.com/photo-1516914943479-89db7d9ae7f2?q=80&w=2732&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
   "https://images.unsplash.com/photo-1531384698654-7f6e477ca221?q=80&w=2800&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
   "https://images.unsplash.com/photo-1531901599143-df5010ab9438?q=80&w=2787&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
   "https://images.unsplash.com/photo-1524255684952-d7185b509571?q=80&w=2787&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
   "https://images.unsplash.com/photo-1588175996685-a40693ee1087?q=80&w=2864&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
   "https://images.unsplash.com/photo-1624561172888-ac93c696e10c?q=80&w=2592&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
   "https://images.unsplash.com/photo-1489424731084-a5d8b219a5bb?q=80&w=2787&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
];

function createImageElement(url, index) {
   const div = document.createElement("div");
   div.className = "relative overflow-hidden   aspect-w-16 aspect-h-9";
   const img = document.createElement("img");
   img.className =
      "lazy-image w-full h-full object-cover transition-opacity duration-300 opacity-0";
   img.dataset.src = url;
   img.alt = `Image ${index + 1}`;
   const placeholder = document.createElement("div");
   placeholder.className = "absolute inset-0 bg-neutral-200 animate-pulse w-full h-full";
   div.appendChild(placeholder);
   div.appendChild(img);
   return div;
}

function lazyLoad() {
   const images = document.querySelectorAll("img.lazy-image");
   const options = {
      root: null,
      rootMargin: "0px",
      threshold: 0.1,
   };
   const imageObserver = new IntersectionObserver((entries, observer) => {
      entries.forEach((entry) => {
         if (entry.isIntersecting) {
            const img = entry.target;
            img.src = img.dataset.src;
            img.onload = () => {
               img.classList.remove("opacity-0");
               img.previousElementSibling.remove(); // Remove placeholder
            };
            observer.unobserve(img);
         }
      });
   }, options);
   images.forEach((img) => imageObserver.observe(img));
}
// Create and append image elements
imageUrls.forEach((url, index) => {
   const imageElement = createImageElement(url, index);
   gallery.appendChild(imageElement);
});
// Initialize lazy loading
lazyLoad();

Conclusion

In this tutorial, we learned how to create a lazy-loading image gallery using Tailwind CSS and JavaScript. We covered topics such as creating a lazy-loading image gallery, handling image loading, and implementing lazy-loading functionality.

Hope you enjoyed this tutorial and have a great day!

/Michael Andreuzza

Did you like this tutorial? Please share it with your friends!

Get lifetime access to every theme available today for $199 $139 and own them forever. Plus, new themes, lifetime updates, use on unlimited projects and enjoy lifetime support.

— No subscription required!