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.

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.