Retour au catalogue

Logo Carousel 3D Tilt

Carousel de logos sur un plan incliné en 3D (perspective + rotateX 16deg). Effet tapis roulant cinématique. Défilement infini Framer Motion avec fade sur les bords et fondu de profondeur en bas.

logo-carouselmedium Both Responsive a11y
minimalelegantcorporateuniversalsaasagencyhorizontal-scroll
Theme
"use client";

import { motion } from "framer-motion";

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

interface LogoCarousel3dTiltProps {
  title?: string;
  logos?: LogoItem[];
  speed?: number;
}

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

export default function LogoCarousel3dTilt({
  title = "Trusted by the best teams in the world",
  logos = [],
  speed = 28,
}: LogoCarousel3dTiltProps) {
  if (logos.length === 0) return null;

  // Quadruple pour un défilement parfaitement fluide (animate 0 → -50%)
  const quad = [...logos, ...logos, ...logos, ...logos];

  return (
    <section
      style={{
        paddingTop: "var(--section-padding-y)",
        paddingBottom: "var(--section-padding-y)",
        background: "var(--color-background)",
        overflow: "hidden",
      }}
    >
      {/* Section label */}
      <motion.p
        initial={{ opacity: 0, y: 12 }}
        whileInView={{ opacity: 1, y: 0 }}
        viewport={{ once: true }}
        transition={{ duration: 0.5, ease: EASE }}
        style={{
          textAlign: "center",
          fontSize: "0.75rem",
          fontWeight: 700,
          textTransform: "uppercase",
          letterSpacing: "0.14em",
          color: "var(--color-foreground-muted)",
          marginBottom: "2.5rem",
          paddingLeft: "var(--container-padding-x)",
          paddingRight: "var(--container-padding-x)",
        }}
      >
        {title}
      </motion.p>

      {/* 3D tilt wrapper */}
      <motion.div
        initial={{ opacity: 0, y: 16 }}
        whileInView={{ opacity: 1, y: 0 }}
        viewport={{ once: true }}
        transition={{ duration: 0.6, delay: 0.1, ease: EASE }}
        style={{
          position: "relative",
          perspective: "800px",
          perspectiveOrigin: "50% 10%",
        }}
      >
        {/* Tilted conveyor plane */}
        <div
          style={{
            transform: "rotateX(16deg)",
            transformOrigin: "50% 0%",
            transformStyle: "preserve-3d",
          }}
        >
          {/* Horizontal fade mask */}
          <div
            style={{
              overflow: "hidden",
              maskImage:
                "linear-gradient(to right, transparent 0%, black 8%, black 92%, transparent 100%)",
              WebkitMaskImage:
                "linear-gradient(to right, transparent 0%, black 8%, black 92%, transparent 100%)",
            }}
          >
            <motion.div
              animate={{ x: ["0%", "-50%"] }}
              transition={{
                duration: speed,
                ease: "linear",
                repeat: Infinity,
                repeatType: "loop",
              }}
              style={{
                display: "flex",
                alignItems: "center",
                width: "max-content",
                willChange: "transform",
                paddingTop: "0.5rem",
                paddingBottom: "3rem",
              }}
            >
              {quad.map((logo, i) => (
                <motion.div
                  key={`${logo.id}-${i}`}
                  whileHover={{ scale: 1.1, color: "var(--color-foreground)" }}
                  transition={{ duration: 0.18 }}
                  style={{
                    padding: "0 2.75rem",
                    flexShrink: 0,
                    display: "flex",
                    alignItems: "center",
                    justifyContent: "center",
                    position: "relative",
                    cursor: "default",
                  }}
                >
                  {/* Vertical divider */}
                  {i > 0 && (
                    <div
                      aria-hidden
                      style={{
                        position: "absolute",
                        left: 0,
                        top: "50%",
                        transform: "translateY(-50%)",
                        width: "1px",
                        height: "1.125rem",
                        background: "var(--color-border)",
                        opacity: 0.6,
                      }}
                    />
                  )}
                  <span
                    style={{
                      fontFamily: "var(--font-sans)",
                      fontSize: "1.0625rem",
                      fontWeight: 600,
                      letterSpacing: "-0.01em",
                      color: "var(--color-foreground-muted)",
                      whiteSpace: "nowrap",
                      transition: "color 0.2s",
                    }}
                  >
                    {logo.name}
                  </span>
                </motion.div>
              ))}
            </motion.div>
          </div>
        </div>

        {/* Bottom depth fade — renforce l'illusion de perspective */}
        <div
          aria-hidden
          style={{
            position: "absolute",
            bottom: 0,
            left: 0,
            right: 0,
            height: "3.5rem",
            pointerEvents: "none",
            background:
              "linear-gradient(to top, var(--color-background) 0%, transparent 100%)",
          }}
        />
      </motion.div>
    </section>
  );
}

Avis

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