Copied SVG to clipboard
Something went wrong
Copied code to clipboard
Something went wrong
Saved to bookmarks!
Removed from bookmarks
Webflow Challenge: Win $5K

Default

User image

Default

Name

  • -€50
    Upgrade to Lifetime
The Vault/

3D Image Carousel

3D Image Carousel

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.13.0/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/ScrollTrigger.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/Draggable.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/InertiaPlugin.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/Observer.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 class="img-carousel__wrap">
  <div data-3d-carousel-wrap="" class="img-carousel__list">
    <div data-3d-carousel-panel="" class="img-carousel__panel">
      <div data-3d-carousel-content="" class="img-carousel__item"><img src="https://cdn.prod.website-files.com/689201b6fcd75d4e39c1bf42/689204d87de67d8f1795c60d_Contemplative%20Portrait%20with%20Bucket%20Hat.avif" class="img-carousel__img"></div>
      <div data-3d-carousel-content="" class="img-carousel__item"><img src="https://cdn.prod.website-files.com/689201b6fcd75d4e39c1bf42/689204d83a1a3815a26f7bd6_Mysterious%20Urban%20Portrait.avif" class="img-carousel__img"></div>
    </div>
    <div data-3d-carousel-panel="" class="img-carousel__panel">
      <div data-3d-carousel-content="" class="img-carousel__item"><img src="https://cdn.prod.website-files.com/689201b6fcd75d4e39c1bf42/689204d819cbb2dbff9b2a64_Moonlit%20Rocky%20Landscape.avif" class="img-carousel__img"></div>
    </div>
    <div data-3d-carousel-panel="" class="img-carousel__panel">
      <div data-3d-carousel-content="" class="img-carousel__item"><img src="https://cdn.prod.website-files.com/689201b6fcd75d4e39c1bf42/689204d8cc5908532a8fd8c6_Mysterious%20Balaclava%20Portrait.avif" class="img-carousel__img"></div>
      <div data-3d-carousel-content="" class="img-carousel__item"><img src="https://cdn.prod.website-files.com/689201b6fcd75d4e39c1bf42/689204d8da35115879d4da77_Futuristic%20Mask%20Portrait.avif" class="img-carousel__img"></div>
    </div>
    <div data-3d-carousel-panel="" class="img-carousel__panel">
      <div data-3d-carousel-content="" class="img-carousel__item"><img src="https://cdn.prod.website-files.com/689201b6fcd75d4e39c1bf42/689204d819cbb2dbff9b2a80_Serene%20Wheat%20Field%20Landscape.avif" class="img-carousel__img"></div>
    </div>
    <div data-3d-carousel-panel="" class="img-carousel__panel">
      <div data-3d-carousel-content="" class="img-carousel__item"><img src="https://cdn.prod.website-files.com/689201b6fcd75d4e39c1bf42/689204d84ab87177abc40d52_Solitude%20in%20White.avif" class="img-carousel__img"></div>
      <div data-3d-carousel-content="" class="img-carousel__item"><img src="https://cdn.prod.website-files.com/689201b6fcd75d4e39c1bf42/689204d8bcd829c22ae071e4_Mysterious%20Portrait.avif" class="img-carousel__img"></div>
    </div>
    <div data-3d-carousel-panel="" class="img-carousel__panel">
      <div data-3d-carousel-content="" class="img-carousel__item"><img src="https://cdn.prod.website-files.com/689201b6fcd75d4e39c1bf42/689204d8f2996c45c7f5ec3e_Modern%20House%20on%20Hillside.avif" class="img-carousel__img"></div>
    </div>
    <div data-3d-carousel-panel="" class="img-carousel__panel">
      <div data-3d-carousel-content="" class="img-carousel__item"><img src="https://cdn.prod.website-files.com/689201b6fcd75d4e39c1bf42/689204d83b775bc909a4914f_Cylindrical%20Tube%20with%20Oranges.avif" class="img-carousel__img"></div>
      <div data-3d-carousel-content="" class="img-carousel__item"><img src="https://cdn.prod.website-files.com/689201b6fcd75d4e39c1bf42/689204d89daf4fb3b1b6dbcf_Contemplative%20Urban%20Portrait.avif" class="img-carousel__img"></div>
    </div>
    <div data-3d-carousel-panel="" class="img-carousel__panel">
      <div data-3d-carousel-content="" class="img-carousel__item"><img src="https://cdn.prod.website-files.com/689201b6fcd75d4e39c1bf42/689204d8cb1cc611680f5233_White%20Bucket%20Hat%20on%20Rocky%20Surface.avif" class="img-carousel__img"></div>
    </div>
    <div data-3d-carousel-panel="" class="img-carousel__panel">
      <div data-3d-carousel-content="" class="img-carousel__item"><img src="https://cdn.prod.website-files.com/689201b6fcd75d4e39c1bf42/689204d82ea2b65ee2abfc33_Urban%20Anonymity.avif" class="img-carousel__img"></div>
      <div data-3d-carousel-content="" class="img-carousel__item"><img src="https://cdn.prod.website-files.com/689201b6fcd75d4e39c1bf42/689204d880f185d08576afa9_Futuristic%20Masked%20Individual.avif" class="img-carousel__img"></div>
    </div>
    <div data-3d-carousel-panel="" class="img-carousel__panel">
      <div data-3d-carousel-content="" class="img-carousel__item"><img src="https://cdn.prod.website-files.com/689201b6fcd75d4e39c1bf42/689204d847d26653710d42aa_Regal%20Portrait%20with%20Crown.avif" class="img-carousel__img"></div>
    </div>
    <div data-3d-carousel-panel="" class="img-carousel__panel">
      <div data-3d-carousel-content="" class="img-carousel__item"><img src="https://cdn.prod.website-files.com/689201b6fcd75d4e39c1bf42/689204d8d7f5ef9c36c91701_Window%20View%20of%20Vibrant%20Sky.avif" class="img-carousel__img"></div>
      <div data-3d-carousel-content="" class="img-carousel__item"><img src="https://cdn.prod.website-files.com/689201b6fcd75d4e39c1bf42/689204d87b92b30ee718d99d_White%20Baseball%20Cap%20with%20Dried%20Plants.avif" class="img-carousel__img"></div>
    </div>
    <div data-3d-carousel-panel="" class="img-carousel__panel">
      <div data-3d-carousel-content="" class="img-carousel__item"><img src="https://cdn.prod.website-files.com/689201b6fcd75d4e39c1bf42/689204d85f8f425e2f133bba_Urban%20Chic%20Portrait.avif" class="img-carousel__img"></div>
    </div>
  </div>
</div>

HTML structure is not required for this resource.

Step 2: Add CSS

CSS

Copy
.img-carousel__wrap {
  justify-content: center;
  align-items: center;
  width: 100%;
  min-height: 100vh;
  display: flex;
}

.img-carousel__list {
  z-index: 1;
  perspective: 90vw;
  perspective-origin: 50%;
  transform-style: preserve-3d;
  justify-content: center;
  align-items: center;
  width: 80vw;
  height: 50vw;
  margin-left: auto;
  margin-right: auto;
  font-size: 1vw;
  display: flex;
  position: relative;
}

.img-carousel__panel {
  z-index: 0;
  flex-direction: column;
  flex: none;
  justify-content: space-between;
  align-items: stretch;
  width: 13em;
  height: 39em;
  display: flex;
  position: absolute;
}

.img-carousel__panel:nth-of-type(even){
  justify-content: center;
}

.img-carousel__item {
  aspect-ratio: 1;
  width: 100%;
  position: relative;
  overflow: hidden;
}

.img-carousel__img {
  object-fit: cover;
  width: 100%;
  max-width: none;
  height: 100%;
  position: absolute;
  inset: 0%;
}

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(Draggable, InertiaPlugin, Observer, ScrollTrigger);
 
function init3dImageCarousel() {
  let radius;
  let draggableInstance;
  let observerInstance;
  let spin;
  let intro;
  let lastWidth = window.innerWidth;
  
  const wrap = document.querySelector('[data-3d-carousel-wrap]');
  if (!wrap) return;

  // Define the radius of your cylinder here
  const calcRadius = () => {
    radius = window.innerWidth * 0.5;
  }
  
  // Destroy function to reset everything on resize
  const destroy = () => {
    draggableInstance && draggableInstance.kill();
    observerInstance && observerInstance.kill();
    spin && spin.kill();
    intro && intro.kill();
    ScrollTrigger.getAll().forEach(st => st.kill());
    const panels = wrap.querySelectorAll('[data-3d-carousel-panel]');
    gsap.set(panels, { clearProps: 'transform' });
  };

  // Create function that sets the spin, drag, and rotation
  const create = () => {
    calcRadius();
    
    const panels = wrap.querySelectorAll('[data-3d-carousel-panel]');
    const content = wrap.querySelectorAll('[data-3d-carousel-content]');
    const proxy = document.createElement('div');
    const wrapProgress = gsap.utils.wrap(0, 1);
    const dragDistance = window.innerWidth * 3; // Control the snapiness on drag
    let startProg;

    // Position panels in 3D space
    panels.forEach(p =>
      p.style.transformOrigin = `50% 50% ${-radius}px`
    );

    // Infinite rotation of all panels
    spin = gsap.fromTo(
      panels,
      { rotationY: i => (i * 360) / panels.length },
      { rotationY: '-=360', duration: 30, ease: 'none', repeat: -1 }
    );
    
    // cheeky workaround to create some 'buffer' when scrolling back up
    spin.progress(1000)

    draggableInstance = Draggable.create(proxy, {
      trigger: wrap,
      type: 'x',
      inertia: true,
      allowNativeTouchScrolling: true,
      onPress() {
        // Subtle feedback on touch/mousedown of the wrap
        gsap.to(content, { 
          clipPath: 'inset(5%)',
          duration: 0.3,
          ease: 'power4.out',
          overwrite: 'auto'
        });
        // Stop automatic spinning to prepare for drag
        gsap.killTweensOf(spin);
        spin.timeScale(0);
        startProg = spin.progress();
      },
      onDrag() {
        const p = startProg + (this.startX - this.x) / dragDistance;
        spin.progress(wrapProgress(p));
      },
      onThrowUpdate() {
        const p = startProg + (this.startX - this.x) / dragDistance;
        spin.progress(wrapProgress(p));
      },
      onRelease() {
        if (!this.tween || !this.tween.isActive()) {
          gsap.to(spin, { timeScale: 1, duration: 0.1 });
        }
        gsap.to(content, {
          clipPath: 'inset(0%)',
          duration: 0.5,
          ease: 'power4.out',
          overwrite: 'auto'
        });
      },
      onThrowComplete() {
        gsap.to(spin, { timeScale: 1, duration: 0.1 });
      }
    })[0];

    // Scroll-into-view animation
    intro = gsap.timeline({
      scrollTrigger: {
        trigger: wrap,
        start: 'top 80%',
        end: 'bottom top',
        scrub: false,
        toggleActions: 'play resume play play'
      },
      defaults: { ease: 'expo.inOut' }
    });
    intro
      .fromTo(spin, { timeScale: 15 }, { timeScale: 1, duration: 2 })
      .fromTo(wrap, { scale: 0.5, rotation: 12 }, { scale: 1, rotation: 5, duration: 1.2 }, '<')
      .fromTo(content, { autoAlpha: 0 }, { autoAlpha: 1, stagger: { amount: 0.8, from: 'random' } }, '<');
  
    // While-scrolling feedback
    observerInstance = Observer.create({
      target: window,
      type: 'wheel,scroll,touch',
      onChangeY: self => {
        // Control how much scroll speed affects the rotation on scroll
        let v = gsap.utils.clamp(-60, 60, self.velocityY * 0.005); 
        spin.timeScale(v);
        const resting = v < 0 ? -1 : 1;
    
        gsap.fromTo(
          { value: v },
          { value: v },
          {
            value: resting,
            duration: 1.2,
            onUpdate() {
              spin.timeScale(this.targets()[0].value);
            }
          }
        );
      }
    });
    
  };
  
  // First create on function call
  create();

  // Debounce function to use on resize events
  const debounce = (fn, ms) => {
    let t;
    return () => {
      clearTimeout(t);
      t = setTimeout(fn, ms);
    };
  };
  
  // Whenever window resizes, first destroy, then re-init it all
  window.addEventListener('resize', debounce(() => {
    const newWidth = window.innerWidth;
    if (newWidth !== lastWidth) {
      lastWidth = newWidth;
      destroy();
      create();
      ScrollTrigger.refresh();
    }
  }, 200));
  
}

document.addEventListener("DOMContentLoaded", () =>{
  init3dImageCarousel();
})

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
.wf-design-mode .img-carousel__panel{
  position: relative; 
}

.wf-design-mode .img-carousel__list{
  overflow: auto;
  justify-content: flex-start;
}

.img-carousel__panel:nth-of-type(even){
  justify-content: center;
}

Implementation

Required HTML Structure & Attributes

<div data-3d-carousel-wrap> 
  <div data-3d-carousel-panel> 
    <div data-3d-carousel-content>
      <!-- your content (image, video, etc) -->
    </div>
  </div>
  <!-- repeat panels -->  
</div>
  • data-3d-carousel-wrap: identifies the main 3D carousel wrapper.
  • data-3d-carousel-panel: each 'slide' in the 3D circle. This can be styled however you like!
  • data-3d-carousel-content: inner container used for press/animation effects. Put your images etc inside of this. If you leave this out, the carousel will still work, it's optional.

Carousel panels

We've used our panels to align some image containers inside. But technically, these panels can be almost anything. Feel free to experiment and try out different types of content and designs!

Carousel perspective

The CSS perspective property is used to define the, well, perspective. Default value is 90vw, it's best to change this to understand and see how this affects the look of your carousel.

Carousel size

The carousel size is not defined with CSS, but in a JS function to be fully dynamic. On default, the radius is defined as half of the window width, but you can change this to whatever you want.

const calcRadius = () => {
  radius = window.innerWidth * 0.5;
}

Drag sensitivity

Defines how 'sensitive' the carousel responds to your drag gestures. A smaller number equals a very sensitive drag, a higher number will make it 'slow' or hard to drag. We also base this off of the window width:

const dragDistance = window.innerWidth * 3; // Control the snapiness on drag

Intro animation

We've used a standard GSAP Timeline to create an 'intro' animation for when the carousel scrolls into view. A nice trick is that you can 'timeScale' the spin animation, to really create a dynamic entrance effect. Since this is a normal GSAP timeline, you can change this however you want.

Scroll behaviour and speed

We've also used GSAP Observer to speed up the spin animation as you're scrolling past the wrapper. You can either completely remove the observerInstance part if you don't want this, or tweak the speed factor in the below variable by changing the 0.005 part. Lower is slower, higher number makes it more sensitive.

// Control how much scroll speed affects the rotation on scroll
let v = gsap.utils.clamp(-60, 60, self.velocityY * 0.005);

Content editing in Webflow

The 2 bits in our 'custom CSS' part (Step 3) of this resource make sure that your panels sit side-by-side inside the Webflow designer, and the wrapper has overflow: auto, so you can scroll the carousel horizontally as it's flat. Our tip to make this easily editable for you client would be to make a component out of the wrapper, so that you can hook all the images up to component props. Technically, it would be possible to make this CMS-powered as well, but because our specific example has even and uneven panels, it's a bit more tricky.

Credits

This CodePen here from the official GSAP account was used as a base for the 3D rotational + drag functionalities. We've adapted it slightly and added the scroll behaviour, intro animation, and made it responsive.

Resource Details

Advanced
GSAP
Draggable
Directional
Scrolling
Looping
Marquee
Animation
3D
Infinite
Inertia
Interactive
Scrolltrigger

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.