
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
<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
<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
.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
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
.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
Last updated
August 6, 2025
Type
The Vault
Category
Sliders & Marquees
Need help?
Join Slack