In this tutorial, we’ll be looking at how to set up this panoramic, curved 3D slider in Bricks builder. This was based on a request from a member of the Bricks Builder Community. After a few hours of research and vibe-coding, I found a way to make it work
It not only plays nicely with the other elements in the section but is also fully responsive from mobile to ultrawide screens.
For this to be possible, we will need GSAP, JavaScript and CSS.
I’ve also taken additional steps to make the code very easy to customize so you can adapt it to your requirements.
*Please note that some details of this tutorial may vary slightly from the video due to updates to the code or general process optimizations beyond the date of recording.
1. Import GSAP
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.6.1/gsap.min.js"></script>
2. Setup your HTML structure
Prepare your HTML structure and assign the corresponding CSS classes as follows:
- <div> .ls-curved-carousel
- <div> .stage
- <div> .ring
- <div> .slide
- <img> .media
- <div> .slide
- <div> .ring
- <div> .stage
- Code Element
3. Add the code snippets
IAdd a code element and clear all unnecessaries. Then paste the following snippets in the areas provided for CSS and JavaScript.
3D Curved Slider CSS
.ls-curved-carousel {
--viewport-height: 40rem;
--viewport-height-m: 35rem;
--perspective: 600px;
--perspective-m: 400px;
--block-offset: 0;
--block-offset-m: 0;
/*You can add a fadeout effect by adding a class named "fadeout" to ls-curved-carousel*/
}
.ls-curved-carousel.fadeout {
--fadeout: linear-gradient(90deg, transparent, white 20%, white 80%, transparent 100%);
}
.ls-curved-carousel {
--fadeout: none;
}
body.bricks-is-frontend .ls-curved-carousel {
position: relative;
width: 100%;
height: var(--viewport-height);
transform-style: preserve-3d;
user-select: none;
overflow: visible;
z-index: 1;
margin-block: var(--block-offset);
}
body.bricks-is-frontend .ls-curved-carousel__stage {
perspective: var(--perspective);
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
.ls-curved-carousel {
-webkit-mask-image: var(--fadeout);
mask-image: var(--fadeout);
}
@media (max-width:767px) {
body.bricks-is-frontend .ls-curved-carousel {
height: var(--viewport-height-m);
margin-block: var(--block-offset-m);
}
body.bricks-is-frontend .ls-curved-carousel__stage {
perspective: var(--perspective-m);
}
}
body.bricks-is-frontend .ls-curved-carousel__ring {
position: absolute;
width: 100%;
height: 100%;
transform-style: preserve-3d;
gap:0;
}
body:not(.bricks-is-frontend) .ls-curved-carousel__ring {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
}
body.bricks-is-frontend .ls-curved-carousel__slide {
position: absolute;
/* Width and height will be set by JavaScript */
transform-style: preserve-3d;
overflow: hidden;
}
body.bricks-is-frontend .ls-curved-carousel__media {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 24px;
}
body.bricks-is-frontend .ls-curved-carousel:after {
content: none;
}
3D Curved Slider JavaScript
/**************************************
* 3D CURVED CAROUSEL CONFIGURATION
**************************************/
const carouselConfig = {
// User-specified parameters
slideHeight: 600, // Height of each slide in pixels
slidesInRing: 21, // Number of slides visible in the ring
slideSpacing: 1, // Spacing between slides in degrees
// Visual settings
radius: 1200, // Radius of the carousel circle (px)
initialRotation: 180, // Initial rotation of the carousel (degrees)
// Animation settings
autoRotate: true, // Set to false to disable auto-rotation
rotationSpeed: 0.1, // Target speed of rotation (degrees per frame)
rotationDirection: 1, // 1 for clockwise, -1 for counter-clockwise
// Interaction settings
pauseOnHover: true, // Pause rotation when hovering
resumeDelay: 0, // Delay before resuming rotation after hover (milliseconds)
pauseEaseDuration: 0.5, // Duration for easing in/out of pause (seconds)
// Entrance animation settings
entranceAnimation: 'fadeIn', // 'fadeIn', 'fadeUp', or 'none'
entranceDuration: 1.5, // Duration of entrance animation (seconds)
entranceStagger: 0.1, // Stagger time between slide animations (seconds)
entranceDistance: 100 // Distance for fadeUp animation (pixels)
};
document.addEventListener('DOMContentLoaded', function() {
// Initialize the carousel
initCarousel();
});
function initCarousel() {
const ring = document.querySelector('.ls-curved-carousel__ring');
const originalSlides = document.querySelectorAll('.ls-curved-carousel__slide');
const originalSlideCount = originalSlides.length;
// Check if we need to duplicate slides
if (originalSlideCount < carouselConfig.slidesInRing) {
duplicateSlides(originalSlides, carouselConfig.slidesInRing - originalSlideCount);
}
// Get the updated slides collection after potential duplication
const slides = document.querySelectorAll('.ls-curved-carousel__slide');
const slideCount = slides.length;
// Calculate angle per slide for even distribution
const anglePerSlide = 360 / carouselConfig.slidesInRing;
// Calculate ideal slide width based on arc length
const arcLength = (anglePerSlide - carouselConfig.slideSpacing) * (Math.PI / 180) * carouselConfig.radius;
const slideWidth = arcLength;
console.log(`Original slides: ${originalSlideCount}, Total slides: ${slideCount}`);
console.log(`Angle per slide: ${anglePerSlide}°, Calculated width: ${slideWidth}px`);
// Set container dimensions
const container = document.querySelector('.ls-curved-carousel__stage');
container.style.width = `${slideWidth}px`;
container.style.height = `${carouselConfig.slideHeight}px`;
// Set up autorotation variables
let autoRotate = carouselConfig.autoRotate;
let targetRotationSpeed = carouselConfig.rotationSpeed; // Target speed
let currentRotationSpeed = 0; // Current speed, managed by GSAP tween
let rotationDirection = carouselConfig.rotationDirection;
let autoRotateTimeout;
let lastUpdateTime = Date.now();
let updateRotationFunction;
let speedTween; // Store the GSAP tween for speed control
// Proxy object for tweening the speed
let speedController = { value: 0 };
// Initialize the carousel timeline
const timeline = gsap.timeline();
// Set initial rotation
timeline.set(ring, { rotationY: carouselConfig.initialRotation });
// Apply calculated dimensions and position all slides in a circle
slides.forEach((slide, index) => {
// Set slide dimensions
slide.style.width = `${slideWidth}px`;
slide.style.height = `${carouselConfig.slideHeight}px`;
// Position slide in 3D space - evenly distribute around the circle
gsap.set(slide, {
rotateY: index * -anglePerSlide,
transformOrigin: `50% 50% ${carouselConfig.radius}px`,
z: -carouselConfig.radius,
backfaceVisibility: 'hidden'
});
});
// Add entrance animation if enabled
if (carouselConfig.entranceAnimation !== 'none') {
let entranceAnimation = {};
switch (carouselConfig.entranceAnimation) {
case 'fadeIn':
entranceAnimation = {
opacity: 0,
duration: carouselConfig.entranceDuration,
stagger: carouselConfig.entranceStagger,
ease: 'power2.out'
};
break;
case 'fadeUp':
entranceAnimation = {
y: carouselConfig.entranceDistance,
opacity: 0,
duration: carouselConfig.entranceDuration,
stagger: carouselConfig.entranceStagger,
ease: 'power3.out'
};
break;
}
timeline.from('.ls-curved-carousel__slide', entranceAnimation)
.add(() => {
startAutoRotation(); // Start rotation after entrance animation
});
} else {
// Start auto-rotation immediately if no entrance animation
startAutoRotation();
}
// Function to update rotation
function updateRotation() {
// Use the current speed from the tweened proxy object
currentRotationSpeed = speedController.value;
if (currentRotationSpeed === 0) return; // If speed is 0, no need to update
const currentTime = Date.now();
const deltaTime = currentTime - lastUpdateTime;
lastUpdateTime = currentTime;
// Calculate rotation based on time passed, current speed, and direction
const rotationAmount = (deltaTime / 16.67) * currentRotationSpeed * rotationDirection;
gsap.to(ring, {
rotationY: `+=${rotationAmount}`,
duration: 0,
overwrite: true // Ensure this tween doesn't interfere with speed tween
});
}
function startAutoRotation() {
if (!autoRotate) return; // Only start if autoRotate is globally enabled
lastUpdateTime = Date.now();
updateRotationFunction = updateRotation;
gsap.ticker.add(updateRotationFunction);
// Kill any existing speed tween before starting a new one
if (speedTween) {
speedTween.kill();
}
// Ease the speed up to the target speed
speedTween = gsap.to(speedController, {
value: targetRotationSpeed,
duration: carouselConfig.pauseEaseDuration,
ease: 'power1.out'
});
}
function stopAutoRotation() {
// Kill any existing speed tween before starting a new one
if (speedTween) {
speedTween.kill();
}
// Ease the speed down to 0
speedTween = gsap.to(speedController, {
value: 0,
duration: carouselConfig.pauseEaseDuration,
ease: 'power1.in',
onComplete: () => {
// Remove ticker only after speed reaches 0
if (updateRotationFunction) {
gsap.ticker.remove(updateRotationFunction);
updateRotationFunction = null; // Clear the reference
}
}
});
}
function resumeAutoRotation() {
// Clear any pending resume timeout
if (autoRotateTimeout) clearTimeout(autoRotateTimeout);
// Set a timeout to restart the rotation after the delay
autoRotateTimeout = setTimeout(() => {
if (carouselConfig.autoRotate) { // Check again if autoRotate is still desired
startAutoRotation(); // This will handle easing the speed up
}
}, carouselConfig.resumeDelay);
}
// Add pause/resume functionality if enabled
if (carouselConfig.pauseOnHover) {
const carouselElement = document.querySelector('.ls-curved-carousel');
carouselElement.addEventListener('mouseenter', () => {
// Clear any pending resume timeout when entering
if (autoRotateTimeout) clearTimeout(autoRotateTimeout);
stopAutoRotation(); // Ease speed down
});
carouselElement.addEventListener('mouseleave', () => {
resumeAutoRotation(); // Start the resume process (with delay)
});
// Also handle touch events for mobile
carouselElement.addEventListener('touchstart', () => {
// Clear any pending resume timeout when touching
if (autoRotateTimeout) clearTimeout(autoRotateTimeout);
stopAutoRotation(); // Ease speed down
}, { passive: true }); // Use passive listener for performance
carouselElement.addEventListener('touchend', () => {
resumeAutoRotation(); // Start the resume process (with delay)
});
}
}
function duplicateSlides(originalSlides, count) {
const ring = document.querySelector('.ls-curved-carousel__ring');
const originalCount = originalSlides.length;
// Clone slides until we have enough
for (let i = 0; i < count; i++) {
const clone = originalSlides[i % originalCount].cloneNode(true);
ring.appendChild(clone);
}
}
Update Log
[April 22, 2025] – Updated the code so that pauseOnHover eases in instead of being so abrupt. Also includes a new configuration setting: pauseEaseDuration: 0.5
(Thanks for the suggestion Fabian)
Curved Panoramic 3D Carousel Slider (Ready-to-use Template + Draggable version)
