# How to Build a Draggable Image Gallery and a Custom Lightbox With GSAP

In a previous tutorial, we learned how to build a responsive image gallery with slick.js. Today, let’s build something similar yet even more complete: a responsive image gallery with a draggable featured image/main slide and a responsive lightbox gallery that sits on top of it. To make the target element draggable, we’ll take advantage of GSAP’s Draggable plugin.

Sounds like a good exercise?

## What We’re Building

Here’s the component that we’re going to create:

Be sure to open the lightbox gallery by clicking on the Open Lightbox button.

## 1. Include the Required Plugins

As already discussed, to make the featured images draggable elements, we’re going to use GSAP and specifically its Draggable plugin.

You can achieve this functionality by using another plugin or even with pure JavaScript events

Optionally, we’ll also include InertiaPlugin (formerly ThrowPropsPlugin), a second GSAP plugin that will apply a momentum-based movement after the mouse/touch is released. It’s worth noting that this is a premium plugin, and you have to sign up for a GSAP membership before deciding to use it. In our case, we’re going to use a trial version that only works locally and on domains like codepen.io (see the browser console of the demo for more details).

With all these in mind, we’ll include three external JavaScript files. The first two are mandatory, while the third one is optional.

## 2. Define the HTML Markup

### Set the Markup for the Gallery

We’ll first define a wrapper element that will contain:

• The lists of thumbnail and featured images. Both lists will include the same Unsplash images. These will have equal dimensions and be big enough to implement the draggable effect.
• The button for opening the lightbox gallery.

By default, the first main slide will appear. But we can configure that behavior by attaching the is-active class to the desired slide (lists).

In addition, all featured images will retain their original dimensions (1920px x 1280px).

Here’s the required structure for our gallery:

 1  

### Set the Markup for the Lightbox Gallery

Next, we'll define a lightbox component that will include:

• A list with the aforementioned Unsplash images. Depending on the active main slide, the related lightbox image will appear.
• The navigation arrows for switching between slides.
• A close button

Here’s the required structure for our lightbox:

 1  

## 3. Specify the Main Styles

With the markup ready, we’ll continue with the main styles of our page. For simplicity, I’ll skip the introductory/reset ones. Also, I won’t optimize or merge the common CSS styles, so it will be easier for you to understand what is going on. Be sure to see all of them by clicking the CSS tab of the demo.

### Set Gallery Layout

The gallery will have a maximum width of 950px.

On large screens (>750px), we’ll have two columns. The thumbnails will appear on the left side, while the featured images will be on the right, like this:

Notice that the thumbnails will cover a quarter of the gallery width, while the featured images will cover three-quarters.

On small screens (≤750px), the thumbnails will sit underneath the featured image, like this:

Notice that each thumbnail will cover one-quarter of the parent’s width.

Here are the associated styles:

 1 .gallery-wrapper {  2  max-width: 950px;  3  padding: 0 15px;  4  margin: 0 auto;  5  display: grid;  6  grid-template-columns: 1fr 3fr;  7  grid-gap: 15px;  8 }  9 10 .gallery-wrapper .thumb-list {  11  display: grid;  12  grid-gap: 15px;  13 }  14 15 @media (max-width: 750px) {  16  .gallery-wrapper {  17  grid-template-columns: 1fr;  18  }  19 20  .gallery-wrapper .thumb-list {  21  grid-template-columns: repeat(4, 1fr);  22  order: 1;  23  }  24 } 

### Featured Slides Visibility

By default, all featured slides will be hidden, apart from the active slide. Plus, only one featured slide (the active one) will appear at a time.

Here are the associated styles:

 1 .gallery-wrapper .featured-list li {  2  opacity: 0;  3  transition: opacity 0.25s;  4 }  5 6 .gallery-wrapper .featured-list li.is-active {  7  opacity: 1;  8 } 

### Position Featured Images

On large screens, both gallery columns will have the same height as they are grid items. The featured images though will be absolutely positioned elements and centered within their container. To view all their parts we have to drag over them.

On small screens, as the columns are stacked and the featured images are still absolutely positioned, we should specify a fixed height for the right column.

Here are the associated styles:

 1 .gallery-wrapper .featured-list {  2  position: relative;  3  overflow: hidden;  4 }  5 6 .gallery-wrapper .featured-list .featured-img {  7  background-size: cover;  8  background-repeat: no-repeat;  9  background-position: center;  10  z-index: 1 !important;  11  position: absolute;  12  top: 50%;  13  left: 50%;  14  transform: translate(-50%, -50%);  15 }  16 17 @media (max-width: 750px) {  18  .gallery-wrapper .featured-list {  19  height: 340px;  20  }  21 } 

### Indicate Active and Hovered States

Each time we hover over a thumbnail, its ::before pseudo-element will appear. This will have a light blue background and sit on top of the thumbnail.

On the other hand, the active thumbnail will receive a red border color.

Here are the associated styles:

 1 /*CUSTOM VARIABLES HERE*/  2 3 .gallery-wrapper .thumb-list li {  4  position: relative;  5  cursor: pointer;  6  border: 4px solid var(--black);  7 }  8 9 .gallery-wrapper .thumb-list li:not(.is-active):hover::before {  10  content: "";  11  position: absolute;  12  top: 0;  13  left: 0;  14  right: 0;  15  bottom: 0;  16  background: var(--hovered-thumb);  17 }  18 19 .gallery-wrapper .thumb-list li.is-active {  20  border-color: var(--red);  21 } 

### Set Lightbox Styles

By default, the lightbox will be hidden and appear only when someone clicks on the corresponding call-to-action button.

Here are some things to note about the lightbox styles:

• The lightbox will be a fixed positioned element with horizontally centered content.
• The navigation and close buttons will be absolutely positioned elements.
• The gallery will be vertically centered and only one of its images will appear at a time. This will depend on the active featured slide.
• The images will have a maximum height equal to the viewport height and their width will be set to auto

Here's a part of these styles:

 1 /*CUSTOM VARIABLES HERE*/  2 3 .lightbox {  4  position: fixed;  5  top: 0;  6  left: 0;  7  right: 0;  8  bottom: 0;  9  display: flex;  10  justify-content: center;  11  opacity: 0;  12  visibility: hidden;  13  z-index: 2;  14  transition: all 0.25s;  15  background: var(--black);  16 }  17 18 .lightbox.is-visible {  19  opacity: 1;  20  visibility: visible;  21 }  22 23 .lightbox-header {  24  position: absolute;  25  top: 0;  26  left: 0;  27  right: 0;  28  display: flex;  29  justify-content: flex-end;  30  padding: 5px 10px;  31  z-index: 2;  32  background: var(--lightbox-header);  33 }  34 35 .lightbox-dialog {  36  display: flex;  37  align-items: center;  38 }  39 40 .lightbox-items {  41  display: grid;  42 }  43 44 .lightbox-items li {  45  display: flex;  46  grid-column: 1;  47  grid-row: 1;  48  opacity: 0;  49  transition: opacity 0.25s;  50 }  51 52 .lightbox-items li.is-active {  53  opacity: 1;  54 }  55 56 .lightbox-items img {  57  width: auto;  58  max-height: 100vh;  59 } 

Let’s now give life to our component!

Again, for simplicity, I won’t optimize/merge the JavaScript code. Feel free to grab the code parts that work for your projects.

### Change Gallery Slides

Each time we click on a thumbnail, we’ll perform the following actions:

• Remove the is-active class from the pre-existing active thumbnail and featured image.
• Find the index of the current active thumbnail.
• Assign the is-active class to the active thumbnail and the featured image whose index matches the index of this thumbnail.

Here’s the required code:

### Close Lightbox

There are two different ways for closing the lightbox:

• Firstly, by clicking on the .close-lightbox element that sits inside the lightbox header.
• Secondly, by pressing the Esc key.
Once again, here's the required code:
 1 ...  2 3 document.addEventListener("click", (e) => {  4  if (e.target === closeLightbox) {  5  body.classList.remove(overflowYHiddenClass);  6  lightbox.classList.remove(isVisibleClass);  7  }  8 });  9 10 document.addEventListener("keyup", (e) => {  11  // Esc  12  if (document.querySelector(".lightbox.is-visible") && e.keyCode === 27) {  13  body.classList.remove(overflowYHiddenClass);  14  lightbox.classList.remove(isVisibleClass);  15  }  16 }); 

### Change Lightbox Slides

Each time we click on a navigation arrow, we’ll perform the following actions:

• Grab a copy of the currently active lightbox slide.
• Remove the is-active class from this slide.
• Check to see which button is clicked. If that’s the next one, we’ll add the is-active class to the slide that follows the active one. If there isn’t such a slide, the first one will receive this class.
• On the other hand, if that's the previous one, we’ll add the is-active class to the slide that precedes the active one. If there isn’t such a slide, the last one will receive this class.

Here's the required code:

 1 ...  2 3 for (const lightboxControl of lightboxControls) {  4  lightboxControl.addEventListener("click", (e) => {  5  const activeSlide = lightboxItems.querySelector("li.is-active");  6  activeSlide.classList.remove(isActiveClass);  7  if (e.currentTarget === lightboxNextControl) {  8  activeSlide.nextElementSibling  9  ? activeSlide.nextElementSibling.classList.add(isActiveClass)  10  : lightboxItems.firstElementChild.classList.add(isActiveClass);  11  } else {  12  activeSlide.previousElementSibling  13  ? activeSlide.previousElementSibling.classList.add(isActiveClass)  14  : lightboxItems.lastElementChild.classList.add(isActiveClass);  15  }  16  });  17 } 

Like we did with the image gallery, let's make our lightbox more robust by adding support for keyboard navigation. More specifically:

• If the lightbox is visible, we check to see if the left () or right () arrow keys are pressed.
• If the left arrow key is pressed, we'll force a click to the previous navigation control.
• In the same way, if the right arrow key is pressed, we'll force a click to the next navigation control.

Here’s the required code:

 1 ...  2 3 document.addEventListener("keyup", (e) => {  4  if (  5  document.querySelector(".lightbox.is-visible") &&  6  (e.keyCode === 37 || e.keyCode === 39)  7  ) {  8  // left arrow  9  if (e.keyCode === 37) {  10  lightboxPrevControl.click();  11  } else {  12  // next arrow  13  lightboxNextControl.click();  14  }  15  }  16 }); 

## Conclusion

Another exercise has come to an end, folks! Thanks for following along. Hopefully, you enjoyed what we built today, and it gave you a solid knowledge of how to combine some custom code with the power of popular plugins like GSAP.

Here’s a reminder of what we built:

Last but not least, remember that GSAP isn’t the only way to create a draggable effect. You’re more than welcome to try another option and share it with us. Also, if you want to effortlessly add swipe support on the lightbox, you might want to try a JavaScript library like Hammer.js.

As always, thanks a lot for reading!