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