This content originally appeared on Envato Tuts+ Tutorials and was authored by George Martsoukos
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.
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:
<div class="gallery-wrapper"> <ul class="thumb-list"> <li class="is-active"> <img width="1920" height="1280" src="sports-car1.jpg" alt=""> </li> <li> <img width="1920" height="1280" src="sports-car2.jpg" alt=""> </li> <li> <img width="1920" height="1280" src="sports-car3.jpg" alt=""> </li> <li> <img width="1920" height="1280" src="sports-car4.jpg" alt=""> </li> </ul> <ul class="featured-list"> <li class="is-active"> <div class="featured-img" style="background-image: url(sports-car1.jpg); width: 1920px; height: 1280px;"></div> </li> <li> <div class="featured-img" style="background-image: url(sports-car2.jpg); width: 1920px; height: 1280px;"></div> </li> <li> <div class="featured-img" style="background-image: url(sports-car3.jpg); width: 1920px; height: 1280px;"></div> </li> <li> <div class="featured-img" style="background-image: url(sports-car4.jpg); width: 1920px; height: 1280px;"></div> </li> </ul> <button type="button" class="open-lightbox">Open Lightbox</button> </div>
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:
<div class="lightbox"> <header class="lightbox-header"> <button type="button" class="close-lightbox" aria-label="Close lightbox">✕</button> </header> <div class="lightbox-dialog"> <section class="lightbox-content"> <ul class="lightbox-items"> <li> <img src="sports-car1.jpg" alt="" width="1920" height="1280"> </li> <li> <img src="sports-car2.jpg" alt="" width="1920" height="1280"> </li> <li> <img src="sports-car3.jpg" alt="" width="1920" height="1280"> </li> <li> <img src="sports-car4.jpg" alt="" width="1920" height="1280"> </li> </ul> <button type="button" class="lightbox-control lightbox-control-next" aria-label="Next slide"> <svg aria-hidden="true" xmlns="https://www.w3.org/2000/svg" width="34" height="34" viewBox="0 0 24 24"> <path d="M5 3l3.057-3 11.943 12-11.943 12-3.057-3 9-9z" /> </svg> </button> <button type="button" class="lightbox-control lightbox-control-prev" aria-label="Previous slide"> <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="34" height="34" viewBox="0 0 24 24"> <path d="M16.67 0l2.83 2.829-9.339 9.175 9.339 9.167-2.83 2.829-12.17-11.996z" /> </svg> </button> </section> </div> </div>
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:
.gallery-wrapper { max-width: 950px; padding: 0 15px; margin: 0 auto; display: grid; grid-template-columns: 1fr 3fr; grid-gap: 15px; } .gallery-wrapper .thumb-list { display: grid; grid-gap: 15px; } @media (max-width: 750px) { .gallery-wrapper { grid-template-columns: 1fr; } .gallery-wrapper .thumb-list { grid-template-columns: repeat(4, 1fr); order: 1; } }
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:
.gallery-wrapper .featured-list li { opacity: 0; transition: opacity 0.25s; } .gallery-wrapper .featured-list li.is-active { opacity: 1; }
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:
.gallery-wrapper .featured-list { position: relative; overflow: hidden; } .gallery-wrapper .featured-list .featured-img { background-size: cover; background-repeat: no-repeat; background-position: center; z-index: 1 !important; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); } @media (max-width: 750px) { .gallery-wrapper .featured-list { height: 340px; } }
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:
/*CUSTOM VARIABLES HERE*/ .gallery-wrapper .thumb-list li { position: relative; cursor: pointer; border: 4px solid var(--black); } .gallery-wrapper .thumb-list li:not(.is-active):hover::before { content: ""; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: var(--hovered-thumb); } .gallery-wrapper .thumb-list li.is-active { border-color: var(--red); }
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:
/*CUSTOM VARIABLES HERE*/ .lightbox { position: fixed; top: 0; left: 0; right: 0; bottom: 0; display: flex; justify-content: center; opacity: 0; visibility: hidden; z-index: 2; transition: all 0.25s; background: var(--black); } .lightbox.is-visible { opacity: 1; visibility: visible; } .lightbox-header { position: absolute; top: 0; left: 0; right: 0; display: flex; justify-content: flex-end; padding: 5px 10px; z-index: 2; background: var(--lightbox-header); } .lightbox-dialog { display: flex; align-items: center; } .lightbox-items { display: grid; } .lightbox-items li { display: flex; grid-column: 1; grid-row: 1; opacity: 0; transition: opacity 0.25s; } .lightbox-items li.is-active { opacity: 1; } .lightbox-items img { width: auto; max-height: 100vh; }
4. Add the JavaScript
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:
const galleryWrapper = document.querySelector(".gallery-wrapper"); const thumbList = galleryWrapper.querySelector(".thumb-list"); const thumbItems = thumbList.querySelectorAll("li"); const featuredList = galleryWrapper.querySelector(".featured-list"); const isActiveClass = "is-active"; thumbItems.forEach((el) => { el.addEventListener("click", () => { thumbList.querySelector("li.is-active").classList.remove(isActiveClass); featuredList.querySelector("li.is-active").classList.remove(isActiveClass); let index = Array.from(thumbItems).indexOf(el); el.classList.add(isActiveClass); featuredList .querySelector(`li:nth-child(${++index})`) .classList.add(isActiveClass); }); });
Add Keyboard Support
Going even further, we'll enhance the previous functionality by providing support for keyboard navigation. More specifically:
- Each time the up (↑) or down (↓) arrow keys are pressed, we’ll retrieve the pre-existing active thumbnail.
- If the up arrow key is pressed, the thumbnail that precedes the current thumbnail will become active. In case there isn’t any such thumbnail, the last thumbnail will become active.
- If the down arrow key is pressed, the thumbnail that follows the current thumbnail will become active. In case there isn’t any such thumbnail, the first thumbnail will become active.
Here’s the required code:
... document.addEventListener("keyup", (e) => { if (e.keyCode === 38 || e.keyCode === 40) { const activeThumb = thumbList.querySelector("li.is-active"); // up arrow if (e.keyCode === 38) { if (activeThumb.previousElementSibling) { activeThumb.previousElementSibling.click(); } else { thumbList.lastElementChild.click(); } } else { // down arrow if (activeThumb.nextElementSibling) { activeThumb.nextElementSibling.click(); } else { thumbList.firstElementChild.click(); } } } });
Make Feature Images Draggable
Coming up next, we’ll make the featured images draggable elements. As already discussed earlier, to do this, we’ll take advantage of GSAP's Draggable plugin. We'll instantiate the plugin through its create()
method and pass it the two following arguments:
- The elements that we want to drag.
- A configuration object. Inside it, we’ll specify the bounds at which the draggable elements should stay during the effect. Optionally, as we’ve loaded the InertiaPlugin, we’ll also request through the
inertia
property momentum-based motion after users’ mouse/touch is released.
Here’s the corresponding code:
const galleryWrapper = document.querySelector(".gallery-wrapper"); const featuredList = galleryWrapper.querySelector(".featured-list"); const featuredItems = featuredList.querySelectorAll("li"); Draggable.create(featuredImgs, { bounds: featuredList, inertia: true });
Of course, here, we covered just the basic part of the plugin’s functionality. You can go even deeper by reading the docs and implementing complex stuff.
Open Lightbox
As already pointed, the lightbox will appear as soon as we click on the Open Lightbox button. So upon click, we’ll perform the following actions:
- Remove the
is-active
class from the pre-existing active lightbox image, if there's any. - Remove the vertical scrollbar from the
body
element through theoverflow-y-hidden
class. - Find the index of the current active featured image.
- Assign the
is-active
class to the lightbox image whose index matches the index of this featured image. - Reveal the lightbox through the
is-visible
class.



Here’s the required code:
... openLightbox.addEventListener("click", () => { if (lightboxItems.querySelector("li.is-active")) { lightboxItems.querySelector("li.is-active").classList.remove(isActiveClass); } body.classList.add(overflowYHiddenClass); const el = featuredList.querySelector("li.is-active"); let index = Array.from(featuredItems).indexOf(el); lightboxItems .querySelector(`li:nth-child(n+${++index})`) .classList.add(isActiveClass); lightbox.classList.add(isVisibleClass); });
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.
... document.addEventListener("click", (e) => { if (e.target === closeLightbox) { body.classList.remove(overflowYHiddenClass); lightbox.classList.remove(isVisibleClass); } }); document.addEventListener("keyup", (e) => { // Esc if (document.querySelector(".lightbox.is-visible") && e.keyCode === 27) { body.classList.remove(overflowYHiddenClass); lightbox.classList.remove(isVisibleClass); } });
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:
... for (const lightboxControl of lightboxControls) { lightboxControl.addEventListener("click", (e) => { const activeSlide = lightboxItems.querySelector("li.is-active"); activeSlide.classList.remove(isActiveClass); if (e.currentTarget === lightboxNextControl) { activeSlide.nextElementSibling ? activeSlide.nextElementSibling.classList.add(isActiveClass) : lightboxItems.firstElementChild.classList.add(isActiveClass); } else { activeSlide.previousElementSibling ? activeSlide.previousElementSibling.classList.add(isActiveClass) : lightboxItems.lastElementChild.classList.add(isActiveClass); } }); }
Add Keyboard Support
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:
... document.addEventListener("keyup", (e) => { if ( document.querySelector(".lightbox.is-visible") && (e.keyCode === 37 || e.keyCode === 39) ) { // left arrow if (e.keyCode === 37) { lightboxPrevControl.click(); } else { // next arrow lightboxNextControl.click(); } } });
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!
This content originally appeared on Envato Tuts+ Tutorials and was authored by George Martsoukos

George Martsoukos | Sciencx (2021-07-26T07:30:27+00:00) How to Build a Draggable Image Gallery and a Custom Lightbox With GSAP. Retrieved from https://www.scien.cx/2021/07/26/how-to-build-a-draggable-javascript-image-gallery-with-gsap/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.