Reusable 3D Rotating Carousel Pattern in React
This is a reusable React + CSS pattern for a 3D rotating carousel of cards.
You can reuse this carousel for projects, testimonials, product highlights, and other grouped content:
- Projects
- Testimonials
- Product highlights
- Feature groups
- Team roles
Here is a quick demo from my portfolio:
Why this pattern works
The carousel uses one simple idea:
- Place each card in a 3D circular ring with
rotateY(...) translateZ(...). - Rotate only the parent container over time.
You avoid per-card animation complexity, while keeping smooth visual motion.
Minimal implementation
import React, { useEffect, useRef } from "react";
const categories = [
{
title: "Projects",
items: ["Portfolio", "DevOps Toolkit", "Realtime Dashboard"]
},
{
title: "Testimonials",
items: ["Client A", "Client B", "Client C"]
},
{
title: "Highlights",
items: ["Scalability", "Observability", "Automation"]
},
{
title: "Products",
items: ["Starter", "Pro", "Enterprise"]
},
{
title: "Roadmap",
items: ["Q1", "Q2", "Q3"]
}
];
export default function ThreeDCarousel() {
const carouselRef = useRef(null);
useEffect(() => {
let angle = 0;
let intervalId = null;
const rotate = () => {
angle += 360 / categories.length;
if (carouselRef.current) {
carouselRef.current.style.transform = `rotateY(-${angle}deg)`;
}
};
const startRotation = () => {
if (!intervalId) intervalId = setInterval(rotate, 2500);
};
const stopRotation = () => {
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
}
};
const node = carouselRef.current;
if (!node) return;
node.addEventListener("mouseenter", stopRotation);
node.addEventListener("mouseleave", startRotation);
startRotation();
return () => {
stopRotation();
node.removeEventListener("mouseenter", stopRotation);
node.removeEventListener("mouseleave", startRotation);
};
}, []);
return (
<section className="carousel-section">
<h2>3D Rotating Carousel</h2>
<div className="carousel-wrapper">
<div className="carousel" ref={carouselRef}>
{categories.map((card, index) => {
const angle = (360 / categories.length) * index;
return (
<article
className="tile"
key={card.title}
style={{ transform: `rotateY(${angle}deg) translateZ(320px)` }}
>
<h3>{card.title}</h3>
<ul>
{card.items.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</article>
);
})}
</div>
</div>
</section>
);
}
CSS for the 3D behavior
.carousel-wrapper {
perspective: 1500px;
perspective-origin: center;
display: flex;
justify-content: center;
height: 560px;
}
.carousel {
position: relative;
transform-style: preserve-3d;
transition: transform 1s ease-in-out;
width: 800px;
height: 100%;
}
.tile {
position: absolute;
top: 50%;
left: 50%;
width: 352px;
height: 352px;
margin-left: -176px;
margin-top: -176px;
border: 1px solid #3a4b66;
border-radius: 8px;
background: linear-gradient(145deg, rgba(25, 35, 65, 1), rgba(18, 28, 50, 0.98));
padding: 16px;
overflow: hidden;
}
Rotation math
If there are N cards:
- Angle between cards =
360 / N - Card i position =
rotateY(i * 360 / N) translateZ(radius) - Each auto-step rotates by one card angle
With 5 cards:
- Card spacing =
72deg - Rotate step =
72deg
Knobs you can tune
translateZ(320px)controls ring radius.setInterval(..., 2500)controls auto-rotate speed.transition: transform 1scontrols smoothness.perspective: 1500pxcontrols depth intensity.
Reusability tips
- Keep data separate from rendering logic.
- Make a generic card schema:
title,subtitle,items, optional media. - Inject custom card content via props if needed.
- Pause on hover so users can read content.
- Add responsive fallback (stack or horizontal slider) for small screens.
This pattern is ideal when you want one component to display multiple grouped datasets with a premium visual feel and minimal animation code.