---
title: 3d Slider
description: A responsive, 3D image/content slider with a smooth, cascaded stacking effect. It uses Framer Motion for the complex 3D transforms (translateX, translateY, rotate) and handles interactivity via global document event listeners for mouse wheel, drag, and touch gestures. The component is styled using Tailwind CSS and is fully self-contained.
---
## Installation
CLI
Manual
```bash
npx lightswind@latest add 3d-slider
```
Copy and paste the following code into your project.
```tsx
"use client";
import React, { useEffect, useRef, CSSProperties } from "react";
// --- Type Definitions ---
export interface SliderItemData {
title: string;
num: string;
imageUrl: string;
data?: any;
}
interface ThreeDSliderProps {
items: SliderItemData[];
speedWheel?: number;
speedDrag?: number;
containerStyle?: CSSProperties;
className?: string;
onItemClick?: (item: SliderItemData, index: number) => void;
}
// ─── Pure DOM card renderer (no React re-renders in the hot path) ────────────
function createCard(item: SliderItemData, index: number): HTMLDivElement {
const card = document.createElement("div");
card.className =
"absolute top-1/2 left-1/2 cursor-pointer select-none rounded-2xl shadow-2xl bg-zinc-900 overflow-hidden border border-white/10";
card.style.cssText = `
--w: clamp(180px, 28vw, 280px);
--h: clamp(240px, 36vw, 380px);
width: var(--w); height: var(--h);
margin-top: calc(var(--h) / -2);
margin-left: calc(var(--w) / -2);
will-change: transform, opacity;
contain: layout style paint;
transition: none;
display: block;
`;
// inner wrapper
const inner = document.createElement("div");
inner.style.cssText = "position:absolute;inset:0;z-index:10;";
inner.dataset.inner = "true";
// gradient overlay
const grad = document.createElement("div");
grad.style.cssText =
"position:absolute;inset:0;z-index:10;background:linear-gradient(to bottom,rgba(0,0,0,.4) 0%,transparent 50%,rgba(0,0,0,.8) 100%);";
// number
const num = document.createElement("div");
num.style.cssText =
"position:absolute;z-index:20;color:rgba(255,255,255,.9);font-weight:900;top:12px;left:20px;font-size:clamp(28px,6vw,64px);letter-spacing:-0.04em;opacity:.8;";
num.textContent = item.num;
// title
const title = document.createElement("div");
title.style.cssText =
"position:absolute;z-index:20;color:#fff;font-weight:700;bottom:20px;left:20px;font-size:clamp(18px,2.5vw,26px);letter-spacing:-0.02em;text-shadow:0 2px 8px rgba(0,0,0,.5);";
title.textContent = item.title;
// image
const img = document.createElement("img");
img.src = item.imageUrl;
img.alt = item.title;
img.loading = index < 3 ? "eager" : "lazy";
img.decoding = "async";
img.style.cssText =
"width:100%;height:100%;object-fit:cover;pointer-events:none;display:block;";
inner.appendChild(grad);
inner.appendChild(num);
inner.appendChild(title);
inner.appendChild(img);
card.appendChild(inner);
return card;
}
// ─── Main Component ───────────────────────────────────────────────────────────
const ThreeDSlider: React.FC = ({
items,
speedWheel = 0.04,
speedDrag = -0.15,
containerStyle = {},
className = "",
onItemClick,
}) => {
const containerRef = useRef(null);
// Keep callback ref stable without causing re-renders
const onItemClickRef = useRef(onItemClick);
useEffect(() => { onItemClickRef.current = onItemClick; }, [onItemClick]);
useEffect(() => {
const container = containerRef.current;
if (!container || items.length === 0) return;
const numItems = items.length;
// ── State (all in plain refs, zero React state) ──
let progress = 50;
let targetProgress = 50;
let isDown = false;
let startX = 0;
let rafId: number | null = null;
let isAnimating = false;
// ── Build DOM cards ──
const cards: HTMLDivElement[] = items.map((item, i) => createCard(item, i));
cards.forEach((card, i) => {
card.addEventListener("click", () => {
const denom = numItems > 1 ? numItems - 1 : 1;
targetProgress = (i / denom) * 100;
startLoop();
onItemClickRef.current?.(items[i], i);
}, { passive: true });
container.appendChild(card);
});
// ── Transform cache to skip identical writes ──
const cache: { tx: string; ty: string; rot: string; z: string; op: string }[] =
items.map(() => ({ tx: "", ty: "", rot: "", z: "", op: "" }));
// ── Core update — pure math, direct DOM writes ──
function update() {
// Responsive lerp: fast while dragging, smooth when releasing
const lerpFactor = isDown ? 1 : 0.1;
progress += (targetProgress - progress) * lerpFactor;
const clamped = Math.max(0, Math.min(100, progress));
const activeFloat = (clamped / 100) * (numItems - 1);
const denom = numItems > 1 ? numItems - 1 : 1;
for (let i = 0; i < numItems; i++) {
const card = cards[i];
if (!card) continue;
const ratio = (i - activeFloat) / denom;
const tx = (ratio * 750).toFixed(2);
const ty = (ratio * 180).toFixed(2);
const rot = (ratio * 110).toFixed(2);
const dist = Math.abs(i - activeFloat);
const z = numItems - dist;
const op = Math.max(0, Math.min(1, (z / numItems) * 3 - 1.8)).toFixed(2);
const zStr = Math.round(z * 10).toString();
const c = cache[i];
if (c.tx !== tx || c.ty !== ty || c.rot !== rot) {
card.style.transform = `translate3d(${tx}%, ${ty}%, 0) rotate(${rot}deg)`;
c.tx = tx; c.ty = ty; c.rot = rot;
}
if (c.z !== zStr) {
card.style.zIndex = zStr;
c.z = zStr;
}
if (c.op !== op) {
card.style.opacity = op;
c.op = op;
}
}
}
// ── Self-terminating RAF loop ──
function loop() {
update();
const diff = Math.abs(targetProgress - progress);
if (diff > 0.01 || isDown) {
rafId = requestAnimationFrame(loop);
} else {
progress = targetProgress;
update();
isAnimating = false;
rafId = null;
}
}
function startLoop() {
if (isAnimating) return;
isAnimating = true;
rafId = requestAnimationFrame(loop);
}
// ── Initial draw ──
update();
// ── Event helpers ──
function getX(e: MouseEvent | TouchEvent): number | undefined {
return "touches" in e ? e.touches[0]?.clientX : (e as MouseEvent).clientX;
}
// ── Wheel ──
function onWheel(e: WheelEvent) {
const delta = e.deltaY * speedWheel;
const next = targetProgress + delta;
if ((next <= 0 && e.deltaY < 0) || (next >= 100 && e.deltaY > 0)) return;
e.preventDefault();
targetProgress = Math.max(0, Math.min(100, next));
startLoop();
}
// ── Drag ──
function onDown(e: MouseEvent | TouchEvent) {
isDown = true;
const x = getX(e);
if (x !== undefined) startX = x;
// Keep loop alive during drag
if (!isAnimating) { isAnimating = true; rafId = requestAnimationFrame(loop); }
}
function onMove(e: MouseEvent | TouchEvent) {
if (!isDown) return;
const x = getX(e);
if (x === undefined) return;
const diff = (x - startX) * speedDrag;
targetProgress = Math.max(0, Math.min(100, targetProgress + diff));
startX = x;
// No startLoop() — already running, loop checks isDown
}
function onUp() {
if (!isDown) return;
isDown = false;
// Don't stop loop; it will self-terminate after settling
}
container.addEventListener("wheel", onWheel, { passive: false });
container.addEventListener("mousedown", onDown, { passive: true });
container.addEventListener("touchstart", onDown, { passive: true });
window.addEventListener("mousemove", onMove, { passive: true });
window.addEventListener("mouseup", onUp, { passive: true });
window.addEventListener("touchmove", onMove, { passive: true });
window.addEventListener("touchend", onUp, { passive: true });
return () => {
container.removeEventListener("wheel", onWheel);
container.removeEventListener("mousedown", onDown);
container.removeEventListener("touchstart", onDown);
window.removeEventListener("mousemove", onMove);
window.removeEventListener("mouseup", onUp);
window.removeEventListener("touchmove", onMove);
window.removeEventListener("touchend", onUp);
if (rafId) cancelAnimationFrame(rafId);
cards.forEach(c => c.remove());
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [items, speedWheel, speedDrag]);
return (
);
};
export default ThreeDSlider;
```
## Usage
```tsx
import ThreeDSlider from './ThreeDSlider'; // Adjust path as needed
```
```tsx
// 1. Define the items data
const sliderItems = [
{ title: "First Item", num: "01", imageUrl: "/images/image1.jpg", data: { id: 1 } },
{ title: "Second Item", num: "02", imageUrl: "/images/image2.jpg", data: { id: 2 } },
{ title: "Third Item", num: "03", imageUrl: "/images/image3.jpg", data: { id: 3 } },
{ title: "Fourth Item", num: "04", imageUrl: "/images/image4.jpg", data: { id: 4 } },
{ title: "Fifth Item", num: "05", imageUrl: "/images/image5.jpg", data: { id: 5 } },
];
// 2. Default Usage
// 3. Custom Interaction Speeds and Click Handler
const handleItemClick = (item, index) => {
console.log(`Clicked item ${item.num}: ${item.title} at index ${index}`);
};
```