Copied SVG to clipboard
Something went wrong
Copied code to clipboard
Something went wrong

Default

User image

Default

Name

  • Osmo Discount
    -25%
The Vault/

Parallax Image Gallery with Thumbnails

Parallax Image Gallery with Thumbnails

Documentation

Webflow

Code

Setup: External Scripts

External Scripts in Webflow

Make sure to always put the External Scripts before the Javascript step of the resource.

In this video you learn where to put these in your Webflow project? Or how to include a paid GSAP Club plugin in your project?

HTML

Copy
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.7/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.7/dist/Observer.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.7/dist/CustomEase.min.js"></script>

Step 1: Copy structure to Webflow

Copy structure to Webflow

In the video below we described how you can copy + paste the structure of this resource to your Webflow project.

Copy to Webflow

Webflow structure is not required for this resource.

Step 1: Add HTML

HTML

Copy
<div data-slideshow="wrap" class="img-slider">
  <div class="img-slider__list">
    <div data-slideshow="slide" class="img-slide is--current"><img data-slideshow="parallax" alt="" src="https://cdn.prod.website-files.com/67db521fe639fd09f860b106/67dbf80921b87fe739e3cbd2_osmo-slideshow-img-3.avif" draggable="false" class="img-slide__inner"></div>
    <div data-slideshow="slide" class="img-slide"><img data-slideshow="parallax" alt="" src="https://cdn.prod.website-files.com/67db521fe639fd09f860b106/67dbf809f124211fa71791fa_osmo-slideshow-img-1.avif"  draggable="false" class="img-slide__inner"></div>
    <div data-slideshow="slide" class="img-slide"><img data-slideshow="parallax" alt="" src="https://cdn.prod.website-files.com/67db521fe639fd09f860b106/67dbf809e5434a14205e65b2_osmo-slideshow-img-2.avif"  draggable="false" class="img-slide__inner"></div>
    <div data-slideshow="slide" class="img-slide"><img data-slideshow="parallax" alt="" src="https://cdn.prod.website-files.com/67db521fe639fd09f860b106/67dbf809fb316a147c9d1dda_osmo-slideshow-img-4.avif"  draggable="false" class="img-slide__inner"></div>
  </div>
  <div class="img-slider__nav">
    <div data-slideshow="thumb" class="img-slider__thumb is--current"><img src="https://cdn.prod.website-files.com/67db521fe639fd09f860b106/67dbf80921b87fe739e3cbd2_osmo-slideshow-img-3.avif" class="slider-thumb__img"></div>
    <div data-slideshow="thumb" class="img-slider__thumb"><img src="https://cdn.prod.website-files.com/67db521fe639fd09f860b106/67dbf809f124211fa71791fa_osmo-slideshow-img-1.avif" class="slider-thumb__img"></div>
    <div data-slideshow="thumb" class="img-slider__thumb"><img src="https://cdn.prod.website-files.com/67db521fe639fd09f860b106/67dbf809e5434a14205e65b2_osmo-slideshow-img-2.avif" class="slider-thumb__img"></div>
    <div data-slideshow="thumb" class="img-slider__thumb"><img src="https://cdn.prod.website-files.com/67db521fe639fd09f860b106/67dbf809fb316a147c9d1dda_osmo-slideshow-img-4.avif" class="slider-thumb__img"></div>
  </div>
</div>

HTML structure is not required for this resource.

Step 2: Add CSS

CSS

Copy
.img-slider {
  grid-column-gap: 1rem;
  grid-row-gap: 1rem;
  border-radius: .5em;
  justify-content: center;
  align-items: flex-end;
  width: 100%;
  height: 100vh;
  display: flex;
  position: relative;
}

.img-slider__list {
  grid-template-rows: 100%;
  grid-template-columns: 100%;
  place-items: center;
  width: 100%;
  height: 100%;
  display: grid;
  overflow: hidden;
}

.img-slide {
  opacity: 0;
  pointer-events: none;
  will-change: transform, opacity;
  grid-area: 1 / 1 / -1 / -1;
  place-items: center;
  width: 100%;
  height: 100%;
  display: grid;
  position: relative;
  overflow: hidden;
}

.img-slide.is--current {
  opacity: 1;
  pointer-events: auto;
}

.img-slide__inner {
  object-fit: cover;
  will-change: transform;
  width: 100%;
  height: 100%;
  position: absolute;
}

.img-slider__nav {
  z-index: 2;
  grid-column-gap: .5rem;
  grid-row-gap: .5rem;
  pointer-events: none;
  flex-flow: wrap;
  justify-content: center;
  align-items: center;
  max-width: 95vw;
  display: flex;
  position: absolute;
  bottom: 2rem;
}

.img-slider__thumb {
  aspect-ratio: 1.5;
  pointer-events: auto;
  cursor: pointer;
  border: 1px solid #fff3;
  border-radius: .3125rem;
  width: 7rem;
  transition: border-color .2s;
  position: relative;
  overflow: hidden;
}

.img-slider__thumb:hover {
  border-color: #fff6;
}

.img-slider__thumb.is--current {
  border-color: #fff;
}

.slider-thumb__img {
  object-fit: cover;
  width: 100%;
  height: 100%;
}

@media screen and (max-width: 991px) {
  .img-slider__list {
    width: 100%;
  }

  .img-slider__thumb {
    flex: none;
  }
}

@media screen and (max-width: 767px) {
  .img-slider__nav {
    flex-flow: wrap;
  }

  .img-slider__thumb {
    border-radius: .25rem;
    width: 5rem;
  }
}

@media screen and (max-width: 479px) {
  .img-slider__thumb {
    width: 4.5rem;
  }
}

Step 2: Add custom Javascript

Custom Javascript in Webflow

In this video, Ilja gives you some guidance about using JavaScript in Webflow:

Step 2: Add Javascript

Step 3: Add Javascript

Javascript

Copy
gsap.registerPlugin(Observer,CustomEase)
CustomEase.create("slideshow-wipe", "0.6, 0.08, 0.02, 0.99");

function initSlideShow(el) {
  
  // Save all elements in an object for easy reference
  const ui = {
    el,
    slides: Array.from(el.querySelectorAll('[data-slideshow="slide"]')),
    inner: Array.from(el.querySelectorAll('[data-slideshow="parallax"]')),
    thumbs: Array.from(el.querySelectorAll('[data-slideshow="thumb"]'))
  };

  let current = 0;
  const length = ui.slides.length;
  let animating = false;
  let observer;
  let animationDuration = 0.9 // Define the duration of your 'slide' here

  ui.slides.forEach((slide, index) => {
    slide.setAttribute('data-index', index);
  });
  ui.thumbs.forEach((thumb, index) => {
    thumb.setAttribute('data-index', index);
  });

  ui.slides[current].classList.add('is--current');
  ui.thumbs[current].classList.add('is--current');

  function navigate(direction, targetIndex = null) {
    if (animating) return;
    animating = true;
    observer.disable();

    const previous = current;
    current =
      targetIndex !== null && targetIndex !== undefined
        ? targetIndex
        : direction === 1
          ? current < length - 1
            ? current + 1
            : 0
          : current > 0
            ? current - 1
            : length - 1;

    const currentSlide = ui.slides[previous];
    const currentInner = ui.inner[previous];
    const upcomingSlide = ui.slides[current];
    const upcomingInner = ui.inner[current];

    gsap.timeline({
      defaults: {
        duration: animationDuration,
        ease: 'slideshow-wipe'
      },
      onStart: function() {
        upcomingSlide.classList.add('is--current');
        ui.thumbs[previous].classList.remove('is--current');
        ui.thumbs[current].classList.add('is--current');
      },
      onComplete: function() {
        currentSlide.classList.remove('is--current');
        animating = false;
        // Re-enable observer after a short delay
        setTimeout(() => observer.enable(), animationDuration);
      }
    })
      .to(currentSlide, { xPercent: -direction * 100 },0)
      .to(currentInner, { xPercent: direction * 50 }, 0)
      .fromTo(upcomingSlide, { xPercent: direction * 100 }, { xPercent: 0 }, 0)
      .fromTo(upcomingInner, { xPercent: -direction * 50 }, { xPercent: 0 }, 0);
  }

  function onClick(event) {
    const targetIndex = parseInt(event.currentTarget.getAttribute('data-index'), 10);
    if (targetIndex === current || animating) return;
    const direction = targetIndex > current ? 1 : -1;
    navigate(direction, targetIndex);
  }
  
  ui.thumbs.forEach(thumb => {
    thumb.addEventListener('click', onClick);
  });

  observer = Observer.create({
    target: el,
    type: 'wheel,touch,pointer',
    // Drag events to go left/right
    onLeft: () => { if (!animating) navigate(1); },
    onRight: () => {if (!animating) navigate(-1); },
    // For wheel events, check horizontal movement
    onWheel: (event) => {
      if (animating) return;
      if (Math.abs(event.deltaX) > Math.abs(event.deltaY)) {
        if (event.deltaX > 50) {
          navigate(1);
        } else if (event.deltaX < -50) {
          navigate(-1);
        }
      }
    },
    wheelSpeed: -1,
    tolerance: 10
  });

  // Cleanup function if you need it
  return {
    destroy: function() {
      if (observer) observer.kill();
      ui.thumbs.forEach(thumb => {
        thumb.removeEventListener('click', onClick);
      });
    }
  };
}

document.addEventListener("DOMContentLoaded", () => {
  
  let wrappers = document.querySelectorAll('[data-slideshow="wrap"]');
  wrappers.forEach(wrap => initSlideShow(wrap));
  
});

Step 3: Add custom CSS

Step 2: Add custom CSS

Custom CSS in Webflow

Curious about where to put custom CSS in Webflow? Ilja explains it in the below video:

CSS

Copy

Implementation

This parallax slider uses GSAP animations and the Observer plugin to create smooth slide transitions triggered by click, wheel, and touch interactions. Here's how you can implement it yourself:

HTML Structure

Use the provided HTML structure:

  • Wrap your slides and thumbnails inside a container with the attribute data-slideshow="wrap".
  • Each slide requires data-slideshow="slide", with the current active slide adding the class is--current.
  • Parallax images within slides should have data-slideshow="parallax".
  • Thumbnails should use data-slideshow="thumb", again marking the active thumbnail with is--current.

CSS Styling

The provided CSS manages layout, responsiveness, and transitions:

  • Slides use CSS Grid to overlap fully, transitioning visibility via opacity.
  • Thumbnails have hover effects and visual indicators for the active state.

JavaScript Functionality

The JavaScript initializes GSAP and the Observer plugin, handling interaction and animation:

  • Initialization: Upon DOMContentLoaded, the slider initializes automatically.
  • Animation: GSAP timelines animate slides horizontally with a parallax effect, using a custom ease.
  • Interaction: Users navigate slides via:
    • Clicking thumbnails to directly select slides.
    • Horizontal scroll or drag gestures to transition slides smoothly.
  • Observer Plugin: Manages intuitive navigation through wheel, touch, and pointer gestures.

Customization Tips

  • Adjusting Speed: Modify animationDuration in the JavaScript to tweak transition speed.
  • Easing: Customize the slideshow-wipe ease for personalized animation.

Webflow and CMS Integration

To use this slider effectively within Webflow CMS:

  • Create two separate CMS Collection Lists: one for the slides, and another for thumbnails.
  • Apply the custom attributes (data-slideshow="slide", data-slideshow="thumb", and data-slideshow="parallax") directly to the CMS Collection List element (not the wrapper div) and to individual items to match the provided HTML structure.
  • Ensure that both CMS lists have the same number of items and are ordered consistently for synchronization.

Resource Details

Background
Custom
Directional
Fullscreen
Infinite
Slideshow
Tabs

Original source

Ilja van Eck

Creator Credits

We always strive to credit creators as accurately as possible. While similar concepts might appear online, we aim to provide proper and respectful attribution.