Blog / CSS/JS

3D Carousel in CSS and JavaScript

Learn to create a rotating 3D carousel with CSS transforms (perspective, preserve-3d, rotateY) and smooth JavaScript navigation. Auto-rotation and touch support included.

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.

💡
Good to know

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.

carousel.html
<!-- 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 perspective property to create depth
  • carousel-3d: the rotating container. It uses transform-style: preserve-3d so its children are positioned in 3D space
  • carousel-cell: each carousel element, positioned with rotateY() and translateZ()

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().

carousel-3d.css
/* 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
🎨
Calculating the radius

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.

JavaScript handles two things: positioning the cells on initialization and managing the rotation when navigation buttons are clicked.

carousel-3d.js
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.

⚠️
Watch out for performance

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.

auto-rotation.js
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.

💡
UX and auto-rotation

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.

touch-support.js
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:

  1. We record the finger's X position on touchstart
  2. We compare it to the position at touchend
  3. If the difference exceeds the threshold (50px), we trigger a rotation
  4. 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.

⚠️
Managing scroll/swipe conflicts

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.

35+ carousel & gallery effects ready to copy

Live preview, customizer, vanilla code — zero dependencies.

Browse carousel effects

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: transform on the carousel container to optimize GPU rendering
  • Avoid complex shadows (box-shadow with high blur) that slow down 3D rendering
  • Prefer transform and opacity for 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

accessibility.css
/* 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-motion by removing the transition and auto-rotation
  • Add ARIA attributes: role="region", aria-label and aria-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
🎨
Go further

Discover other interactive 3D effects in our effects library, with one-click copyable code and live demos.