Retour au catalogue
Gallery 3D Carousel
Carrousel 3D rotatif avec perspective. Les cartes tournent autour de l'axe Y dans un espace 3D avec navigation par fleches.
gallerycomplex Both Responsive a11y
boldelegantagencyportfoliobeautycarousel
Theme
"use client";
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { ChevronLeft, ChevronRight } from "lucide-react";
interface CarouselItem {
title: string;
description: string;
image: string;
}
interface Gallery3dCarouselProps {
title?: string;
subtitle?: string;
items?: CarouselItem[];
}
const EASE = [0.16, 1, 0.3, 1] as const;
export default function Gallery3dCarousel({
title = "Nos realisations",
subtitle = "Portfolio",
items = [],
}: Gallery3dCarouselProps) {
const [activeIndex, setActiveIndex] = useState(0);
const count = items.length;
const prev = () => setActiveIndex((i) => (i - 1 + count) % count);
const next = () => setActiveIndex((i) => (i + 1) % count);
return (
<section
style={{
paddingTop: "var(--section-padding-y, 6rem)",
paddingBottom: "var(--section-padding-y, 6rem)",
background: "var(--color-background)",
overflow: "hidden",
}}
>
<div style={{ maxWidth: "var(--container-max-width, 72rem)", margin: "0 auto", padding: "0 var(--container-padding-x, 1.5rem)" }}>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, ease: EASE }}
style={{ textAlign: "center", marginBottom: "4rem" }}
>
<p style={{ fontSize: "0.8125rem", fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.1em", color: "var(--color-accent)", marginBottom: "0.5rem" }}>
{subtitle}
</p>
<h2 style={{ fontFamily: "var(--font-sans)", fontSize: "clamp(2rem, 4vw, 3.5rem)", fontWeight: 700, letterSpacing: "-0.03em", color: "var(--color-foreground)" }}>
{title}
</h2>
</motion.div>
<div style={{ position: "relative", height: "420px", perspective: "1200px" }}>
{items.map((item, i) => {
const offset = i - activeIndex;
const absOffset = Math.abs(offset);
const isActive = offset === 0;
return (
<motion.div
key={item.title + i}
animate={{
rotateY: offset * 45,
z: isActive ? 0 : -200 * absOffset,
x: `${offset * 35}%`,
opacity: absOffset > 2 ? 0 : 1 - absOffset * 0.3,
scale: isActive ? 1 : 0.85,
}}
transition={{ duration: 0.7, ease: EASE }}
style={{
position: "absolute",
top: 0,
left: "50%",
width: "340px",
marginLeft: "-170px",
height: "100%",
transformStyle: "preserve-3d",
cursor: isActive ? "default" : "pointer",
zIndex: count - absOffset,
}}
onClick={() => !isActive && setActiveIndex(i)}
>
<div
style={{
width: "100%",
height: "100%",
borderRadius: "var(--radius-xl, 1.5rem)",
overflow: "hidden",
border: isActive ? "2px solid var(--color-accent)" : "1px solid var(--color-border)",
background: "var(--color-background-card)",
boxShadow: isActive ? "0 24px 64px color-mix(in srgb, var(--color-accent) 15%, transparent)" : "0 8px 32px color-mix(in srgb, var(--color-foreground) 8%, transparent)",
}}
>
<div style={{ width: "100%", height: "65%", overflow: "hidden" }}>
<img src={item.image} alt={item.title} style={{ width: "100%", height: "100%", objectFit: "cover", display: "block" }} />
</div>
<div style={{ padding: "1.25rem" }}>
<h3 style={{ fontSize: "1.125rem", fontWeight: 700, color: "var(--color-foreground)", marginBottom: "0.375rem" }}>{item.title}</h3>
<p style={{ fontSize: "0.8125rem", color: "var(--color-foreground-muted)", lineHeight: 1.5 }}>{item.description}</p>
</div>
</div>
</motion.div>
);
})}
<button onClick={prev} style={{ position: "absolute", left: "1rem", top: "50%", transform: "translateY(-50%)", zIndex: 20, width: "48px", height: "48px", borderRadius: "50%", border: "1px solid var(--color-border)", background: "var(--color-background-card)", display: "flex", alignItems: "center", justifyContent: "center", cursor: "pointer" }}>
<ChevronLeft style={{ width: 20, height: 20, color: "var(--color-foreground)" }} />
</button>
<button onClick={next} style={{ position: "absolute", right: "1rem", top: "50%", transform: "translateY(-50%)", zIndex: 20, width: "48px", height: "48px", borderRadius: "50%", border: "1px solid var(--color-border)", background: "var(--color-background-card)", display: "flex", alignItems: "center", justifyContent: "center", cursor: "pointer" }}>
<ChevronRight style={{ width: 20, height: 20, color: "var(--color-foreground)" }} />
</button>
</div>
<div style={{ display: "flex", justifyContent: "center", gap: "0.5rem", marginTop: "2rem" }}>
{items.map((_, i) => (
<button
key={i}
onClick={() => setActiveIndex(i)}
style={{
width: i === activeIndex ? "2rem" : "0.5rem",
height: "0.5rem",
borderRadius: "var(--radius-full, 9999px)",
border: "none",
background: i === activeIndex ? "var(--color-accent)" : "var(--color-border)",
cursor: "pointer",
transition: "all 0.3s ease",
}}
/>
))}
</div>
</div>
</section>
);
}