Retour au catalogue

Logo Carousel 3D Cylinder

Carousel de logos en cylindre 3D avec rotation continue, reflexion au sol, et ralentissement au survol. Perspective CSS et transforms 3D.

logo-carouselcomplex Both Responsive a11y
boldelegantsaasagencyuniversalcentered
Theme
"use client";

import { motion, useMotionValue, useTransform, animate } from "framer-motion";
import { useEffect, useState } from "react";

interface Logo {
  name: string;
}

interface LogoCarousel3dCylinderProps {
  title?: string;
  description?: string;
  logos?: Logo[];
  radius?: number;
}

const EASE = [0.16, 1, 0.3, 1] as const;

export default function LogoCarousel3dCylinder({
  title = "Ils nous font confiance",
  description = "Plus de 500 entreprises utilisent notre solution.",
  logos = [],
  radius = 260,
}: LogoCarousel3dCylinderProps) {
  const [isHovered, setIsHovered] = useState(false);
  const rotation = useMotionValue(0);
  const count = logos.length || 1;
  const angleStep = 360 / count;

  useEffect(() => {
    const controls = animate(rotation, rotation.get() + 360, {
      duration: isHovered ? 40 : 20,
      ease: "linear",
      repeat: Infinity,
    });
    return () => controls.stop();
  }, [isHovered, rotation]);

  return (
    <section
      style={{
        padding: "var(--section-padding-y) 0",
        background: "var(--color-background)",
        overflow: "hidden",
      }}
    >
      <div
        style={{
          maxWidth: "var(--container-max-width)",
          margin: "0 auto",
          padding: "0 var(--container-padding-x)",
        }}
      >
        {/* Header */}
        <motion.div
          initial={{ opacity: 0, y: 16 }}
          animate={{ opacity: 1, y: 0 }}
          transition={{ duration: 0.5, ease: EASE }}
          style={{ textAlign: "center", marginBottom: "3rem" }}
        >
          <h2
            style={{
              fontFamily: "var(--font-sans)",
              fontSize: "clamp(1.75rem, 3vw, 2.5rem)",
              fontWeight: 700,
              color: "var(--color-foreground)",
              marginBottom: "0.75rem",
              letterSpacing: "-0.02em",
            }}
          >
            {title}
          </h2>
          <p
            style={{
              fontSize: "1.0625rem",
              color: "var(--color-foreground-muted)",
              maxWidth: 480,
              margin: "0 auto",
              lineHeight: 1.6,
            }}
          >
            {description}
          </p>
        </motion.div>

        {/* 3D Cylinder */}
        <div
          onMouseEnter={() => setIsHovered(true)}
          onMouseLeave={() => setIsHovered(false)}
          style={{
            perspective: "1000px",
            display: "flex",
            justifyContent: "center",
            alignItems: "center",
            height: 320,
            position: "relative",
          }}
        >
          <motion.div
            style={{
              rotateY: rotation,
              transformStyle: "preserve-3d",
              width: radius * 2,
              height: 60,
              position: "relative",
            }}
          >
            {logos.map((logo, i) => {
              const angle = angleStep * i;
              return (
                <motion.div
                  key={i}
                  initial={{ opacity: 0 }}
                  animate={{ opacity: 1 }}
                  transition={{ delay: i * 0.05 }}
                  style={{
                    position: "absolute",
                    left: "50%",
                    top: "50%",
                    transform: `rotateY(${angle}deg) translateZ(${radius}px) translate(-50%, -50%)`,
                    backfaceVisibility: "hidden",
                  }}
                >
                  <div
                    style={{
                      padding: "0.75rem 1.5rem",
                      borderRadius: "var(--radius-lg)",
                      background: "var(--color-background-card)",
                      border: "1px solid var(--color-border)",
                      whiteSpace: "nowrap",
                      boxShadow: "0 4px 12px rgba(0,0,0,0.06)",
                    }}
                  >
                    <span
                      style={{
                        fontFamily: "var(--font-sans)",
                        fontSize: "0.9375rem",
                        fontWeight: 600,
                        color: "var(--color-foreground)",
                        letterSpacing: "-0.01em",
                      }}
                    >
                      {logo.name}
                    </span>
                  </div>
                </motion.div>
              );
            })}
          </motion.div>

          {/* Floor reflection */}
          <div
            aria-hidden
            style={{
              position: "absolute",
              bottom: 0,
              left: "50%",
              transform: "translateX(-50%)",
              width: radius * 2 + 100,
              height: 80,
              background:
                "radial-gradient(ellipse at center, var(--color-accent-subtle) 0%, transparent 70%)",
              opacity: 0.3,
              filter: "blur(20px)",
              pointerEvents: "none",
            }}
          />
        </div>
      </div>
    </section>
  );
}

Avis

Logo Carousel 3D Cylinder — React Logo-carousel Section — Incubator