Retour au catalogue

Logo Carousel 3D

Carousel de logos en cylindre 3D rotatif. Les logos tournent en cercle avec perspective et rotateY. Ralentit au hover. Visuellement impressionnant.

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

import { useRef, useState, useEffect } from "react";
import { motion, useAnimationFrame } from "framer-motion";

interface LogoItem {
  name: string;
  initials: string;
}

interface LogoCarousel3DProps {
  title?: string;
  logos?: LogoItem[];
  radius?: number;
}

export default function LogoCarousel3D({
  title = "Ils nous font confiance",
  logos = [],
  radius = 280,
}: LogoCarousel3DProps) {
  const angleRef = useRef(0);
  const speedRef = useRef(0.3);
  const [angle, setAngle] = useState(0);
  const [isHovered, setIsHovered] = useState(false);
  const [mounted, setMounted] = useState(false);

  useEffect(() => setMounted(true), []);

  useAnimationFrame((_, delta) => {
    const target = isHovered ? 0.06 : 0.3;
    speedRef.current += (target - speedRef.current) * 0.05;
    angleRef.current += speedRef.current * (delta / 16);
    setAngle(angleRef.current);
  });

  const count = logos.length || 1;
  const step = 360 / count;

  return (
    <section
      style={{
        paddingTop: "var(--section-padding-y-lg)",
        paddingBottom: "var(--section-padding-y-lg)",
        background: "var(--color-background)",
        overflow: "hidden",
      }}
    >
      <div
        style={{
          maxWidth: "var(--container-max-width)",
          margin: "0 auto",
          padding: "0 var(--container-padding-x)",
        }}
      >
        <motion.p
          initial={{ opacity: 0, y: 12 }}
          whileInView={{ opacity: 1, y: 0 }}
          viewport={{ once: true }}
          transition={{ duration: 0.5, ease: [0.16, 1, 0.3, 1] }}
          style={{
            textAlign: "center",
            fontSize: "0.75rem",
            fontWeight: 700,
            textTransform: "uppercase",
            letterSpacing: "0.12em",
            color: "var(--color-foreground-muted)",
            marginBottom: "3.5rem",
            fontFamily: "var(--font-sans)",
          }}
        >
          {title}
        </motion.p>

        {/* 3D Cylinder */}
        <div
          onMouseEnter={() => setIsHovered(true)}
          onMouseLeave={() => setIsHovered(false)}
          style={{
            perspective: "900px",
            display: "flex",
            justifyContent: "center",
            alignItems: "center",
            height: "220px",
            position: "relative",
          }}
        >
          {/* Fade edges */}
          <div
            aria-hidden
            style={{
              position: "absolute",
              inset: 0,
              pointerEvents: "none",
              zIndex: 2,
              background:
                "linear-gradient(90deg, var(--color-background) 0%, transparent 15%, transparent 85%, var(--color-background) 100%)",
            }}
          />

          <div
            style={{
              transformStyle: "preserve-3d",
              transform: `rotateY(${mounted ? angle : 0}deg)`,
              width: 0,
              height: 0,
              position: "relative",
            }}
          >
            {logos.map((logo, i) => {
              const rotateY = step * i;
              return (
                <div
                  key={i}
                  style={{
                    position: "absolute",
                    left: "50%",
                    top: "50%",
                    transform: `rotateY(${rotateY}deg) translateZ(${radius}px) translate(-50%, -50%)`,
                    backfaceVisibility: "hidden",
                  }}
                >
                  <div
                    style={{
                      display: "flex",
                      flexDirection: "column",
                      alignItems: "center",
                      justifyContent: "center",
                      gap: "0.75rem",
                      width: "130px",
                      height: "100px",
                      borderRadius: "var(--radius-lg)",
                      border: "1px solid var(--color-border)",
                      background: "var(--color-background-card)",
                      boxShadow: "0 4px 24px rgba(0,0,0,0.08)",
                      transition: "box-shadow 0.3s, border-color 0.3s",
                    }}
                  >
                    <span
                      style={{
                        fontFamily: "var(--font-sans)",
                        fontSize: "1.125rem",
                        fontWeight: 800,
                        letterSpacing: "0.04em",
                        color: "var(--color-accent)",
                      }}
                    >
                      {logo.initials}
                    </span>
                    <span
                      style={{
                        fontSize: "0.6875rem",
                        fontWeight: 500,
                        color: "var(--color-foreground-muted)",
                        whiteSpace: "nowrap",
                      }}
                    >
                      {logo.name}
                    </span>
                  </div>
                </div>
              );
            })}
          </div>
        </div>
      </div>
    </section>
  );
}

Avis

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