Retour au catalogue
Social Proof Animated Logos
Logos clients qui se dessinent au scroll via SVG path animation (strokeDasharray/strokeDashoffset). Chaque logo s'anime quand visible avec useInView et stagger.
social-proofmedium Both Responsive a11y
minimalelegantcorporatesaasagencyuniversalcentered
Theme
"use client";
import { useRef } from "react";
import { motion, useInView } from "framer-motion";
interface LogoShape {
name: string;
path: string;
viewBox?: string;
}
interface SocialProofAnimatedLogosProps {
title?: string;
subtitle?: string;
logos?: LogoShape[];
}
const EASE = [0.16, 1, 0.3, 1] as const;
function AnimatedLogo({ logo, delay }: { logo: LogoShape; delay: number }) {
const ref = useRef<HTMLDivElement>(null);
const inView = useInView(ref, { once: true, margin: "-40px" });
return (
<motion.div
ref={ref}
initial={{ opacity: 0, y: 16 }}
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay, ease: EASE }}
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: "0.75rem",
}}
>
<div
style={{
width: "80px",
height: "80px",
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "var(--radius-lg, 12px)",
background: "var(--color-background-card, var(--color-background-alt))",
border: "1px solid var(--color-border)",
transition: "border-color var(--duration-normal) var(--ease-out)",
}}
>
<svg
viewBox={logo.viewBox ?? "0 0 24 24"}
fill="none"
style={{ width: "36px", height: "36px" }}
>
<motion.path
d={logo.path}
stroke="var(--color-foreground-muted)"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
initial={{ pathLength: 0, opacity: 0 }}
animate={inView ? { pathLength: 1, opacity: 1 } : {}}
transition={{
pathLength: { duration: 1.4, delay: delay + 0.2, ease: "easeInOut" },
opacity: { duration: 0.3, delay: delay + 0.1 },
}}
/>
</svg>
</div>
<span
style={{
fontSize: "0.75rem",
fontWeight: 500,
color: "var(--color-foreground-muted)",
letterSpacing: "0.02em",
}}
>
{logo.name}
</span>
</motion.div>
);
}
export default function SocialProofAnimatedLogos({
title = "Preuve sociale",
subtitle = "",
logos = [],
}: SocialProofAnimatedLogosProps) {
const ref = useRef<HTMLElement>(null);
const inView = useInView(ref, { once: true, margin: "-60px" });
return (
<section
ref={ref}
style={{
padding: "var(--section-padding-y) 0",
background: "var(--color-background)",
}}
>
<div
style={{
maxWidth: "var(--container-max-width)",
margin: "0 auto",
padding: "0 var(--container-padding-x)",
textAlign: "center",
}}
>
{subtitle && (
<motion.p
initial={{ opacity: 0 }}
animate={inView ? { opacity: 1 } : {}}
transition={{ duration: 0.4, ease: EASE }}
style={{
fontSize: "0.75rem",
fontWeight: 600,
textTransform: "uppercase",
letterSpacing: "0.1em",
color: "var(--color-accent)",
marginBottom: "0.75rem",
}}
>
{subtitle}
</motion.p>
)}
<motion.h2
initial={{ opacity: 0, y: 16 }}
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5, delay: 0.05, ease: EASE }}
style={{
fontFamily: "var(--font-sans)",
fontSize: "clamp(1.5rem, 2.5vw, 2rem)",
fontWeight: 700,
color: "var(--color-foreground)",
marginBottom: "3rem",
letterSpacing: "-0.02em",
}}
>
{title}
</motion.h2>
<div
style={{
display: "flex",
flexWrap: "wrap",
justifyContent: "center",
gap: "2rem",
}}
>
{logos.map((logo, i) => (
<AnimatedLogo key={logo.name} logo={logo} delay={0.08 * i} />
))}
</div>
</div>
</section>
);
}