Introduction
The 3D carousel is a spectacular visual component that gives your interface a true sense of depth. Unlike classic sliders that slide left to right, a 3D carousel rotates elements in space, creating an immersive experience.
In this tutorial, we will build a complete 3D carousel step by step. We will cover the HTML structure, CSS 3D transforms, JavaScript navigation, auto-rotation and touch support for mobile.
CSS 3D transforms are supported by all modern browsers. The key properties are perspective, transform-style: preserve-3d and the functions rotateY() / translateZ().
1. HTML Structure
The structure is simple: a scene container that defines the perspective, a carousel container that rotates, and cells positioned in 3D space.
<!-- Scene container: defines perspective -->
<div class="carousel-scene">
<!-- Carousel container: rotates in 3D -->
<div class="carousel-3d" id="carousel">
<div class="carousel-cell">1</div>
<div class="carousel-cell">2</div>
<div class="carousel-cell">3</div>
<div class="carousel-cell">4</div>
<div class="carousel-cell">5</div>
<div class="carousel-cell">6</div>
</div>
</div>
<!-- Navigation buttons -->
<div class="carousel-nav">
<button onclick="rotateCarousel(-1)">Previous</button>
<button onclick="rotateCarousel(1)">Next</button>
</div>
The three structure levels
- carousel-scene: the 3D viewport. It defines the
perspectiveproperty to create depth - carousel-3d: the rotating container. It uses
transform-style: preserve-3dso its children are positioned in 3D space - carousel-cell: each carousel element, positioned with
rotateY()andtranslateZ()
2. CSS 3D Transforms
The heart of the carousel relies on CSS 3D transforms. Each cell is first rotated around the Y axis at a precise angle, then pushed outward with translateZ().
/* Scene: defines the 3D perspective */
.carousel-scene {
width: 320px;
height: 220px;
perspective: 1000px;
position: relative;
}
/* Carousel: preserve-3d for 3D space */
.carousel-3d {
width: 100%;
height: 100%;
position: relative;
transform-style: preserve-3d;
transition: transform 1s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Cell: absolute positioning + centered */
.carousel-cell {
position: absolute;
width: 140px;
height: 180px;
left: 50%;
top: 50%;
margin-left: -70px;
margin-top: -90px;
border-radius: 16px;
backface-visibility: hidden;
}
Understanding perspective and preserve-3d
The perspective property controls the intensity of the 3D effect. A smaller value (e.g. 500px) creates a more pronounced depth effect, while a larger value (e.g. 2000px) gives a flatter rendering.
- perspective: 1000px: balanced value, good depth without excessive distortion
- transform-style: preserve-3d: essential for children to be in the parent's 3D space
- backface-visibility: hidden: hides the back faces of cards for a clean render
The radius (translateZ) is calculated based on the number of cells and their width. The formula is: radius = (width / 2) / tan(PI / nbCells). For 6 cells of 140px, the radius is approximately 121px.
Cell positioning
Each cell is positioned by combining rotateY() and translateZ(). The angle between each cell is 360 / numberOfCells degrees.
3. JavaScript Navigation
JavaScript handles two things: positioning the cells on initialization and managing the rotation when navigation buttons are clicked.
const carousel = document.getElementById('carousel');
const cells = carousel.querySelectorAll('.carousel-cell');
const cellCount = cells.length;
const theta = 360 / cellCount;
const cellWidth = 140;
// Optimal radius calculation
const radius = Math.round(
(cellWidth / 2) /
Math.tan(Math.PI / cellCount)
);
let currentIndex = 0;
// Position each cell in 3D space
cells.forEach((cell, i) => {
const angle = theta * i;
cell.style.transform =
`rotateY(${angle}deg) translateZ(${radius}px)`;
});
// Carousel rotation
function rotateCarousel(direction) {
currentIndex += direction;
const angle = theta * currentIndex * -1;
carousel.style.transform =
`rotateY(${angle}deg)`;
}
How the rotation works
On each click, we increment or decrement currentIndex. The container's rotation angle is simply theta * currentIndex * -1. The negative sign is important: to show the next element (to the right), the container must rotate in the opposite direction.
The CSS transition transition: transform 1s cubic-bezier(0.4, 0, 0.2, 1) ensures a smooth rotation with a natural easing.
CSS 3D transforms are GPU-accelerated, but too many cells with complex shadows can impact performance. Limit yourself to 6-8 cells maximum for smooth rendering on all devices.
4. Auto-rotation
To make the carousel more dynamic, let's add automatic rotation that pauses when the user interacts with the component.
let autoRotateInterval = null;
const AUTO_ROTATE_DELAY = 3000; // 3 secondes
// Start auto-rotation
function startAutoRotate() {
stopAutoRotate();
autoRotateInterval = setInterval(() => {
rotateCarousel(1);
}, AUTO_ROTATE_DELAY);
}
// Stop auto-rotation
function stopAutoRotate() {
if (autoRotateInterval) {
clearInterval(autoRotateInterval);
autoRotateInterval = null;
}
}
// Pause on hover
const scene = document.querySelector('.carousel-scene');
scene.addEventListener('mouseenter', stopAutoRotate);
scene.addEventListener('mouseleave', startAutoRotate);
// Pause on button click
document.querySelectorAll('.carousel-btn').forEach(btn => {
btn.addEventListener('click', () => {
stopAutoRotate();
// Resume after 5 seconds of inactivity
setTimeout(startAutoRotate, 5000);
});
});
// Start on load
startAutoRotate();
The pattern is classic: a setInterval that calls rotateCarousel(1) every 3 seconds. It stops on mouse hover or when navigation buttons are clicked, and resumes automatically after an inactivity delay.
Auto-rotation is useful to capture attention, but can be frustrating if the user is trying to read the content. Always provide an explicit pause mechanism and resume rotation only after a sufficient delay.
5. Touch/Swipe support
For an optimal mobile experience, let's add swipe gesture detection. The user will be able to rotate the carousel by swiping across the screen with their finger.
let touchStartX = 0;
let touchEndX = 0;
const SWIPE_THRESHOLD = 50; // pixels minimum
scene.addEventListener('touchstart', (e) => {
touchStartX = e.changedTouches[0].screenX;
stopAutoRotate();
}, { passive: true });
scene.addEventListener('touchend', (e) => {
touchEndX = e.changedTouches[0].screenX;
handleSwipe();
}, { passive: true });
function handleSwipe() {
const diff = touchStartX - touchEndX;
if (Math.abs(diff) > SWIPE_THRESHOLD) {
if (diff > 0) {
// Swipe left = next element
rotateCarousel(1);
} else {
// Swipe right = previous element
rotateCarousel(-1);
}
}
// Resume auto-rotation after 5s
setTimeout(startAutoRotate, 5000);
}
Swipe logic
The operation is straightforward:
- We record the finger's X position on
touchstart - We compare it to the position at
touchend - If the difference exceeds the threshold (50px), we trigger a rotation
- A swipe to the left (positive diff) shows the next element
The { passive: true } option on event listeners improves performance by telling the browser that preventDefault() will not be called.
If your carousel is integrated in a scrollable page, horizontal swipe can interfere with vertical scroll. Add direction detection to only trigger rotation if the movement is mostly horizontal.
Best practices
Before wrapping up, here are some recommendations for creating a performant and accessible 3D carousel:
Performance
- Limit the number of cells to 6-8 maximum to avoid slowdowns on mobile
- Use
will-change: transformon the carousel container to optimize GPU rendering - Avoid complex shadows (
box-shadowwith high blur) that slow down 3D rendering - Prefer
transformandopacityfor animations, as they do not trigger reflow
Design
- Dark background recommended: 3D perspective stands out better on dark backgrounds
- Appropriate size: adjust the perspective and radius so cells don't overlap excessively
- Visual indicator: add dots or a counter to show the current position
- Smooth transition: use a custom cubic-bezier rather than a generic
ease
Accessibility
/* Disable animations for sensitive users */
@media (prefers-reduced-motion: reduce) {
.carousel-3d {
transition: none;
}
}
/* Visible focus for keyboard navigation */
.carousel-btn:focus-visible {
outline: 2px solid #6366f1;
outline-offset: 2px;
}
/* ARIA labels for screen readers */
/* <div role="region" aria-label="Carousel 3D"> */
/* <button aria-label="Previous element"> */
/* <button aria-label="Next element"> */
- Respect
prefers-reduced-motionby removing the transition and auto-rotation - Add ARIA attributes:
role="region",aria-labelandaria-roledescription="carousel" - Keyboard navigation: left/right arrows should allow navigation in the carousel
Conclusion
The 3D carousel is a visually impressive component that combines CSS 3D transforms and JavaScript to create a unique interactive experience. By mastering perspective, preserve-3d, rotateY() and translateZ(), you can build interfaces that stand out.
Key takeaways:
- The perspective defines the intensity of the 3D effect
- The radius is calculated based on the number and size of cells
- Auto-rotation must pause during user interactions
- Touch support is essential for a complete mobile experience
- Accessibility must never be neglected, even on visual components
Discover other interactive 3D effects in our effects library, with one-click copyable code and live demos.