JavaScript Image Gallery with Lightbox

JavaScript Image Gallery Lightbox
Project: Async Lightbox Gallery - Vanilla JS
Author: Dario Ferderber
Edit Online: View on CodePen
License: MIT

An image gallery with a lightbox helps users easily explore the visual content of a website. The lightbox provides a quick way to switch between the next and previous images by click clicking the arrow buttons. Whether you are working on a gallery project or looking for a ready-to-use gallery plugin, this JavaScript image gallery lightbox might be helpful for you.

This gallery plugin is purely built with JavaScript classes and comes with easy-to-customize configuration options. You can turn on/off a specific function by passing a true/false value in its configuration options.

Main Features

  • Swipe on mobile enabled
  • Close on the Esc key
  • Navigate with keyboard arrows
  • Responsive design and dependency-free

How to Create JavaScript Image Gallery Lightbox

1. Create a div element with a class name "gallery" and place your images with a class name "gallery__Image" for each img tag inside it. Similarly, define the alt attribute of each image that will be shown as an image caption over the current image.

You can place your image anywhere on the website and add it to the gallery with your image selector (default: .gallery__Image).

<div class="container">
  <div class="gallery">
    <div>
      <img class="gallery__Image" src="https://i.ibb.co/qMvYq77/1.jpg" alt="lorem" data-description="1st Image ---- original width: 1280px" data-large="https://i.ibb.co/NTpRRr9/1.jpg"> original width: 640px
    </div>

    <div>
      <img class="gallery__Image" src="https://i.ibb.co/dD9cd41/2.jpg" alt="lorem" data-description="Image #2 ---- original width: 1280px" data-large="https://i.ibb.co/8dpjd2j/2.jpg"> original width: 640px
    </div>

    <div>
      <img class="gallery__Image" src="https://i.ibb.co/zVhYbT0/3.jpg" alt="lorem" data-description="3rd Image ---- original width: 1280px" data-large="https://i.ibb.co/Y8RYNBd/3.jpg"> original width: 640px
    </div>

    <div>
      <img class="gallery__Image" src="https://i.ibb.co/TWHBbRk/4.jpg" alt="lorem" data-description="4 lorem ipsum ---- original width: 1280px" data-large="https://i.ibb.co/DpxZXqC/4.jpg"> original width: 640px
    </div>

    <div>
      <img class="gallery__Image" src="https://i.ibb.co/5kJtq7Q/5.jpg" alt="lorem" data-description="5th and last ---- original width: 1280px" data-large="https://i.ibb.co/yBxvRgj/5.jpg"> original width: 640px
    </div>
  </div>

2. Now, add the following CSS styles to your project.

.asyncGallery {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  opacity: 0;
  z-index: 1000;
  visibility: hidden;
  background-color: rgba(0, 0, 0, 0.95);
  transition: opacity 200ms, visibility 200ms;
}

.asyncGallery.is-visible {
  opacity: 1;
  visibility: visible;
}

.asyncGallery__Item {
  position: absolute;
  top: 50%;
  left: 50%;
  opacity: 0;
  visibility: hidden;
  overflow: hidden;
  transform: translate(-50%, -50%);
  transition: opacity 200ms, visibility 200ms;
}

.asyncGallery__Item.is-visible {
  opacity: 1;
  visibility: visible;
}

.asyncGallery__ItemImage img {
  max-height: 80vh;
  display: block;
}

.asyncGallery__ItemDescription,
.asyncGallery__Loader {
  color: #fff;
}

.asyncGallery__Loader {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  display: none;
  color: #fff;
  z-index: 100;
}

.asyncGallery__Loader.is-visible {
  display: block;
}

.asyncGallery button {
  background-color: transparent;
  border: 0;
  outline: 0;
  padding: 0;
  font-size: 0;
  cursor: pointer;
}

.asyncGallery__Close {
  position: absolute;
  top: 40px;
  right: 40px;
  width: 30px;
  height: 30px;
  z-index: 1000;
  background-repeat: no-repeat;
  background-size: 30px 30px;
  background-image: url("data:image/svg+xml;utf8;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCFET0NUWVBFIHN2ZyBQVUJMSUMgIi0vL1czQy8vRFREIFNWRyAxLjEvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkIj4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB3aWR0aD0iNTEycHgiIHZlcnNpb249IjEuMSIgaGVpZ2h0PSI1MTJweCIgdmlld0JveD0iMCAwIDY0IDY0IiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCA2NCA2NCI+CiAgPGc+CiAgICA8cGF0aCBmaWxsPSIjRkZGRkZGIiBkPSJNMjguOTQxLDMxLjc4NkwwLjYxMyw2MC4xMTRjLTAuNzg3LDAuNzg3LTAuNzg3LDIuMDYyLDAsMi44NDljMC4zOTMsMC4zOTQsMC45MDksMC41OSwxLjQyNCwwLjU5ICAgYzAuNTE2LDAsMS4wMzEtMC4xOTYsMS40MjQtMC41OWwyOC41NDEtMjguNTQxbDI4LjU0MSwyOC41NDFjMC4zOTQsMC4zOTQsMC45MDksMC41OSwxLjQyNCwwLjU5YzAuNTE1LDAsMS4wMzEtMC4xOTYsMS40MjQtMC41OSAgIGMwLjc4Ny0wLjc4NywwLjc4Ny0yLjA2MiwwLTIuODQ5TDM1LjA2NCwzMS43ODZMNjMuNDEsMy40MzhjMC43ODctMC43ODcsMC43ODctMi4wNjIsMC0yLjg0OWMtMC43ODctMC43ODYtMi4wNjItMC43ODYtMi44NDgsMCAgIEwzMi4wMDMsMjkuMTVMMy40NDEsMC41OWMtMC43ODctMC43ODYtMi4wNjEtMC43ODYtMi44NDgsMGMtMC43ODcsMC43ODctMC43ODcsMi4wNjIsMCwyLjg0OUwyOC45NDEsMzEuNzg2eiIvPgogIDwvZz4KPC9zdmc+Cg==");
}

.asyncGallery__Counter {
  position: absolute;
  font-size: 20px;
  font-weight: bold;
  color: #fff;
  right: 40px;
  bottom: 40px;
}

.asyncGallery__Dots {
  position: absolute;
  left: 50%;
  bottom: 40px;
  display: flex;
  margin: 0;
  padding: 0;
  transform: translateX(-50%);
  list-style-type: none;
  z-index: 1000;
}

.asyncGallery__Dots button {
  padding: 0;
  width: 10px;
  height: 10px;
  background-color: #fff;
  border: 0;
  outline: 0;
  border-radius: 50%;
}

.asyncGallery__Dots li {
  opacity: 0.2;
  transition: opacity 200ms;
}

.asyncGallery__Dots li + li {
  margin-left: 10px;
}

.asyncGallery__Dots li.is-active {
  opacity: 1;
}

.asyncGallery__Next,
.asyncGallery__Prev {
  position: absolute;
  top: 50%;
  width: 30px;
  height: 30px;
  z-index: 1000;
  transition: transform 200ms, opacity 200ms;
  transform: translateY(-50%);
}

.asyncGallery__Next:disabled,
.asyncGallery__Prev:disabled {
  opacity: 0.2;
  cursor: default;
}

.asyncGallery__Next:before,
.asyncGallery__Prev:before {
  position: absolute;
  content: "";
  top: 50%;
  left: 50%;
  background-image: url("data:image/svg+xml,%3Csvg version='1.1' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 129 129' xmlns:xlink='http://www.w3.org/1999/xlink' enable-background='new 0 0 129 129'%3E%3Cg%3E%3Cpath d='m40.4,121.3c-0.8,0.8-1.8,1.2-2.9,1.2s-2.1-0.4-2.9-1.2c-1.6-1.6-1.6-4.2 0-5.8l51-51-51-51c-1.6-1.6-1.6-4.2 0-5.8 1.6-1.6 4.2-1.6 5.8,0l53.9,53.9c1.6,1.6 1.6,4.2 0,5.8l-53.9,53.9z' fill='%23fff'/%3E%3C/g%3E%3C/svg%3E%0A");
  width: 30px;
  height: 30px;
  background-repeat: no-repeat;
  background-size: 30px 30px;
}

.asyncGallery__Next {
  right: 40px;
}

.asyncGallery__Next:hover {
  transform: translateX(2px) translateY(-50%);
}

.asyncGallery__Next:before {
  transform: translate3d(-50%, -50%, 0);
}

.asyncGallery__Prev {
  left: 40px;
}

.asyncGallery__Prev:hover {
  transform: translateX(-2px) translateY(-50%);
}

.asyncGallery__Prev:before {
  transform: translate3d(-50%, -50%, 0) scale(-1);
}

/* DEMO */
* {
  font-family: monospace;
  box-sizing: border-box;
}

body {
  margin: 0;
}

.container {
  max-width: 1200px;
  margin: 0 auto;
}

.gallery {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
}

.gallery div {
  max-width: calc(33.333% - 40px);
  margin: 20px;
  transition: opacity 200ms;
  cursor: pointer;
}

.gallery div:hover {
  opacity: 0.8;
}

.gallery div img {
  max-width: 100%;
}

@media screen and (max-width: 768px) {
  .asyncGallery__Close {
    top: 15px;
    right: 15px;
    width: 20px;
    height: 20px;
    background-size: 20px;
  }

  .asyncGallery__Dots {
    bottom: 15px;
  }

  .asyncGallery__Counter {
    right: 15px;
    bottom: 15px;
    font-size: 12px;
  }

  .asyncGallery__Item {
    width: 100%;
  }

  .asyncGallery__ItemImage img {
    max-height: none;
    max-width: 100%;
  }

  .asyncGallery__ItemDescription {
    padding: 0 20px;
  }

  .asyncGallery__Next,
  .asyncGallery__Prev {
    display: none;
  }

  /* DEMO */
  .gallery {
    display: block;
  }

  .gallery div {
    max-width: 100%;
    margin: 20px 0 0;
  }

  .gallery div img {
    max-width: 100%;
    min-width: 100%;
  }
}

3. Finally, add the following JavaScript gallery plugin before the closing of the body tag and done.

class AsyncGallery {
  constructor(settings) {
    this.settings = {
      images: ".gallery__Image",
      loop: true,
      next: undefined,
      prev: undefined,
      dots: undefined,
      close: undefined,
      loader: undefined,
      counter: undefined,
      counterDivider: "/",
      keyboardNavigation: true,
      hiddenElements: []
    };

    Object.assign(this.settings, settings);

    this.gallery = null;
    this.index = 0;
    this.items = [...document.querySelectorAll(this.settings.images)];

    this.addedItems = {};

    this.touch = {
      endX: 0,
      startX: 0
    };

    this.init();
  }

  get loading() {
    return !this.settings.hiddenElements.includes("loader");
  }

  get dotsVisible() {
    return !this.settings.hiddenElements.includes("dots");
  }

  init() {
    this.clearUncomplete();
    this.createElements();
    this.bindEvents();
  }

  clearUncomplete() {
    this.items = this.items.filter(item => {
      return item.dataset.large;
    });
  }

  createElements() {
    this.gallery = document.createElement("DIV");
    this.gallery.classList.add("asyncGallery");

    this.createSingleElement({
      element: "prev",
      type: "BUTTON",
      event: "click",
      func: this.getPrevious
    });

    this.createSingleElement({
      element: "next",
      type: "BUTTON",
      event: "click",
      func: this.getNext
    });

    this.createSingleElement({
      element: "close",
      type: "BUTTON",
      event: "click",
      func: this.closeGallery
    });

    this.createSingleElement({
      element: "loader",
      type: "SPAN",
      text: "Loading..."
    });

    this.createSingleElement({
      element: "counter",
      type: "SPAN",
      text: "0/0"
    });

    this.createSingleElement({
      element: "dots",
      type: "UL",
      text: ""
    });

    if (!this.settings.hiddenElements.includes("dots")) {
      this.items.forEach((item, i) => {
        let dot = document.createElement("LI");
        dot.dataset.index = i;
        let button = document.createElement("BUTTON");
        button.innerHTML = i;
        button.addEventListener("click", () => {
          this.index = i;
          this.getItem(i);
        });

        dot.append(button);
        this.dots.append(dot);
      });
    }

    window.document.body.append(this.gallery);
  }

  createSingleElement({ element, type, event = "click", func, text }) {
    if (!this.settings.hiddenElements.includes(element)) {
      if (!this.settings[element]) {
        this[element] = document.createElement(type);
        this[element].classList.add(
          `asyncGallery__${this.capitalizeFirstLetter(element)}`
        );
        this[element].innerHTML = text !== undefined ? text : element;
        this.gallery.append(this[element]);
      } else {
        this[element] = document.querySelector(this.settings[element]);
        this.gallery.append(this[element]);
      }

      if (func) {
        this[element].addEventListener(event, func.bind(this));
      }
    }
  }

  getItem(i, content = null) {
    let contentObj = content;
    if (contentObj === null) {
      contentObj = {};
      contentObj.src = this.items[i].dataset.large;
      contentObj.description = this.items[i].dataset.description;
    }

    if (!this.settings.hiddenElements.includes("counter")) {
      this.counter.innerHTML = `
          <span class="asyncGallery__Current">${this.index + 1}</span>${
        this.settings.counterDivider
      }<span class="asyncGallery__Current">${this.items.length}</span>
          `;
    }

    if (!this.addedItems.hasOwnProperty(i)) {
      let image = document.createElement("IMG");

      let galleryItem = document.createElement("DIV");
      galleryItem.classList.add("asyncGallery__Item");

      if (this.loading) {
        this.loader.classList.add("is-visible");
      }

      this.clearVisible();

      if (this.dotsVisible) {
        this.gallery
          .querySelector(`.asyncGallery__Dots li[data-index="${i}"]`)
          .classList.add("is-active");
      }

      image.src = contentObj.src;
      image.alt = contentObj.description ? contentObj.description : "";

      galleryItem.innerHTML = `
          <div class="asyncGallery__ItemImage">
            ${image.outerHTML}
          </div>
          `;

      if (contentObj.description) {
        galleryItem.innerHTML += `
            <div class="asyncGallery__ItemDescription">
              <p>${contentObj.description}</p>
            </div>
            `;
      }

      this.gallery.append(galleryItem);
      this.addedItems[i] = galleryItem;

      image.addEventListener("load", () => {
        this.addedItems[i].loaded = true;
        if (!this.gallery.querySelector(".asyncGallery__Item.is-visible")) {
          this.addedItems[i].classList.add("is-visible");
        }

        if (this.loading) {
          this.loader.classList.remove("is-visible");
        }
      });
    } else {
      this.clearVisible();
      if (this.addedItems[this.index].loaded) {
        this.addedItems[this.index].classList.add("is-visible");
        if (this.loading) {
          this.loader.classList.remove("is-visible");
        }
      } else if (this.loading) {
        this.loader.classList.add("is-visible");
      }

      if (this.dotsVisible) {
        this.gallery
          .querySelector(`.asyncGallery__Dots li[data-index="${i}"]`)
          .classList.add("is-active");
      }
    }

    if (!this.settings.loop) {
      if (this.index === 0) this.prev.setAttribute("disabled", true);
      else this.prev.removeAttribute("disabled");

      if (this.index === this.items.length - 1)
        this.next.setAttribute("disabled", true);
      else this.next.removeAttribute("disabled");
    }
  }

  clearVisible() {
    if (this.gallery.querySelector(".asyncGallery__Item.is-visible")) {
      this.gallery
        .querySelector(".asyncGallery__Item.is-visible")
        .classList.remove("is-visible");
    }

    if (this.gallery.querySelector(".asyncGallery__Dots li.is-active")) {
      this.gallery
        .querySelector(".asyncGallery__Dots li.is-active")
        .classList.remove("is-active");
    }
  }

  closeGallery() {
    this.gallery.classList.remove("is-visible");
    this.clearVisible();
  }

  capitalizeFirstLetter(string) {
    return string.charAt(0).toUpperCase() + string.slice(1);
  }

  handleGesure() {
    if (this.touch.endX > this.touch.startX + 20) {
      this.getPrevious();
    } else if (this.touch.endX < this.touch.startX - 20) {
      this.getNext();
    }
  }

  getPrevious() {
    if (this.settings.loop) {
      this.index--;
      if (this.index === -1) {
        this.index = this.items.length - 1;
      }
      this.getItem(this.index);
    } else if (this.index > 0) {
      this.index--;
      this.getItem(this.index);
    }
  }

  getNext() {
    if (this.settings.loop) {
      this.index++;
      if (this.index === this.items.length) {
        this.index = 0;
      }
      this.getItem(this.index);
    } else if (this.index < this.items.length - 1) {
      this.index++;
      this.getItem(this.index);
    }
  }

  bindEvents() {
    this.items.forEach((item, i) => {
      item.addEventListener("click", e => {
        this.gallery.classList.add("is-visible");
        this.index = i;
        this.getItem(i, {
          src: e.target.dataset.large,
          description: e.target.dataset.description
        });
      });
    });

    document.addEventListener("keyup", e => {
      if (this.gallery.classList.contains("is-visible")) {
        if (e.key === "Escape") this.closeGallery();
        if (this.settings.keyboardNavigation) {
          if (e.keyCode === 39) this.getNext();
          else if (e.keyCode === 37) this.getPrevious();
        }
      }
    });

    this.gallery.addEventListener(
      "touchstart",
      e => {
        this.touch.startX = e.changedTouches[0].screenX;
      },
      false
    );

    this.gallery.addEventListener(
      "touchend",
      e => {
        this.touch.endX = e.changedTouches[0].screenX;
        this.handleGesure();
      },
      false
    );
  }
}

new AsyncGallery();

If you want to customize the working of the gallery plugin, you can use the following available options:

  1. images – selector for images you want to have in gallery
  2. loop – true/false
  3. next – selector for your Next button
  4. prev – selector for your Previous button
  5. close – selector for your Close button
  6. loader – selector for your loading animation/text,
  7. counter – selector for your counter,
  8. counterDivider – charachter between current number and number of images
  9. keyboardNavigation: enable/disable keyboard arrows navigation
  10. hiddenElements – list for elements you don’t want to show (next, prev, close, loader)

That’s all! hopefully, you have successfully created JavaScript image gallery with lightbox. If you have any questions or suggestions, feel free to comment below.

Leave a Comment

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply