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