Retour au catalogue
About Numbers Animated
About en chiffres avec compteurs animes au scroll : employes, clients, pays, projets.
aboutmedium Both Responsive a11y
boldcorporateuniversalsaasagencysplit
Theme
"use client";
import { useEffect, useRef, useState } from "react";
import { motion, useInView } from "framer-motion";
import { TrendingUp } from "lucide-react";
interface Metric {
value: number;
suffix: string;
label: string;
}
interface AboutNumbersAnimatedProps {
sectionTitle?: string;
sectionSubtitle?: string;
description?: string;
metrics?: Metric[];
}
const EASE = [0.16, 1, 0.3, 1] as const;
function AnimatedCounter({ value, suffix }: { value: number; suffix: string }) {
const [count, setCount] = useState(0);
const ref = useRef<HTMLSpanElement>(null);
const isInView = useInView(ref, { once: true });
useEffect(() => {
if (!isInView) return;
const duration = 1500;
const steps = 40;
const increment = value / steps;
let current = 0;
const timer = setInterval(() => {
current += increment;
if (current >= value) {
setCount(value);
clearInterval(timer);
} else {
setCount(Math.floor(current));
}
}, duration / steps);
return () => clearInterval(timer);
}, [isInView, value]);
return (
<span ref={ref}>
{count}
{suffix}
</span>
);
}
export default function AboutNumbersAnimated({
sectionTitle = "En chiffres",
sectionSubtitle = "Nos resultats parlent d'eux-memes",
description = "Description de l'entreprise.",
metrics = [],
}: AboutNumbersAnimatedProps) {
return (
<section
style={{
paddingTop: "var(--section-padding-y)",
paddingBottom: "var(--section-padding-y)",
background: "var(--color-background-alt)",
}}
>
<div
style={{
maxWidth: "var(--container-max-width)",
margin: "0 auto",
padding: "0 var(--container-padding-x)",
}}
>
<div
style={{
display: "grid",
gridTemplateColumns: "1fr 1fr",
gap: "4rem",
alignItems: "center",
}}
>
{/* Left: text */}
<motion.div
initial={{ opacity: 0, y: 16 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, ease: EASE }}
>
<div
style={{
display: "inline-flex",
alignItems: "center",
gap: "0.5rem",
marginBottom: "1rem",
}}
>
<TrendingUp style={{ width: 18, height: 18, color: "var(--color-accent)" }} />
<span
style={{
fontSize: "0.75rem",
fontWeight: 600,
textTransform: "uppercase",
letterSpacing: "0.08em",
color: "var(--color-accent)",
}}
>
{sectionSubtitle}
</span>
</div>
<h2
style={{
fontFamily: "var(--font-sans)",
fontSize: "clamp(1.75rem, 3vw, 2.5rem)",
fontWeight: 700,
color: "var(--color-foreground)",
lineHeight: 1.15,
marginBottom: "1.25rem",
}}
>
{sectionTitle}
</h2>
<p
style={{
fontSize: "1.0625rem",
lineHeight: 1.7,
color: "var(--color-foreground-muted)",
}}
>
{description}
</p>
</motion.div>
{/* Right: metrics grid */}
<div
style={{
display: "grid",
gridTemplateColumns: "1fr 1fr",
gap: "1.5rem",
}}
>
{metrics.map((metric, i) => (
<motion.div
key={i}
initial={{ opacity: 0, y: 16 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.45, delay: i * 0.08, ease: EASE }}
style={{
padding: "1.75rem",
borderRadius: "var(--radius-lg)",
border: "1px solid var(--color-border)",
background: "var(--color-background-card)",
textAlign: "center",
}}
>
<p
style={{
fontFamily: "var(--font-sans)",
fontSize: "clamp(2rem, 4vw, 3rem)",
fontWeight: 700,
color: "var(--color-accent)",
lineHeight: 1,
marginBottom: "0.5rem",
fontVariantNumeric: "tabular-nums",
}}
>
<AnimatedCounter value={metric.value} suffix={metric.suffix} />
</p>
<p
style={{
fontSize: "0.875rem",
fontWeight: 500,
color: "var(--color-foreground-muted)",
}}
>
{metric.label}
</p>
</motion.div>
))}
</div>
</div>
</div>
</section>
);
}