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

Avis