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