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>
  );
}

Avis

Gallery 3D Carousel — React Gallery Section — Incubator