Retour au catalogue

Values Animated

Valeurs avec animation de compteur au scroll. Chiffres cles mis en avant avec descriptions.

valuesmedium Both Responsive a11y
boldcorporateuniversalsaasgrid
Theme
"use client";

import { useRef } from "react";
import { motion, useInView } from "framer-motion";
import { Sparkles } from "lucide-react";

interface AnimatedValue {
  label: string;
  number: number;
  suffix?: string;
  description: string;
}

interface ValuesAnimatedProps {
  title?: string;
  subtitle?: string;
  badge?: string;
  values?: AnimatedValue[];
}

const ease: [number, number, number, number] = [0.16, 1, 0.3, 1];

function CounterNumber({ target, suffix = "" }: { target: number; suffix?: string }) {
  const ref = useRef<HTMLSpanElement>(null);
  const isInView = useInView(ref, { once: true });

  return (
    <motion.span
      ref={ref}
      initial={{ opacity: 0 }}
      animate={isInView ? { opacity: 1 } : {}}
      className="text-4xl md:text-5xl font-bold tabular-nums"
      style={{ color: "var(--color-accent)" }}
    >
      {isInView && (
        <motion.span
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          transition={{ duration: 0.3 }}
        >
          {target.toLocaleString("fr-FR")}
          {suffix}
        </motion.span>
      )}
    </motion.span>
  );
}

export default function ValuesAnimated({
  title = "Nos chiffres",
  subtitle = "",
  badge = "",
  values = [],
}: ValuesAnimatedProps) {
  return (
    <section className="py-20 lg:py-28 px-6" style={{ background: "var(--color-background)" }}>
      <div className="mx-auto max-w-5xl">
        <motion.div
          initial={{ opacity: 0, y: 16 }}
          whileInView={{ opacity: 1, y: 0 }}
          transition={{ duration: 0.5, ease }}
          viewport={{ once: true }}
          className="text-center mb-16"
        >
          {badge && (
            <span
              className="inline-flex items-center gap-1.5 mb-4 px-3 py-1 text-xs font-medium tracking-wider uppercase rounded-full"
              style={{ color: "var(--color-accent)", border: "1px solid var(--color-border)" }}
            >
              <Sparkles size={12} />
              {badge}
            </span>
          )}
          <h2 className="text-3xl md:text-4xl font-bold" style={{ color: "var(--color-foreground)" }}>
            {title}
          </h2>
          {subtitle && (
            <p className="mt-3 text-base max-w-xl mx-auto" style={{ color: "var(--color-foreground-muted)" }}>
              {subtitle}
            </p>
          )}
        </motion.div>

        <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
          {values.map((value, i) => (
            <motion.div
              key={i}
              initial={{ opacity: 0, y: 24 }}
              whileInView={{ opacity: 1, y: 0 }}
              transition={{ duration: 0.5, ease, delay: i * 0.12 }}
              viewport={{ once: true }}
              className="text-center p-6 rounded-xl"
              style={{ background: "var(--color-background-card)", border: "1px solid var(--color-border)" }}
            >
              <CounterNumber target={value.number} suffix={value.suffix} />
              <h3 className="mt-3 text-sm font-bold uppercase tracking-wider" style={{ color: "var(--color-foreground)" }}>
                {value.label}
              </h3>
              <p className="mt-2 text-sm leading-relaxed" style={{ color: "var(--color-foreground-muted)" }}>
                {value.description}
              </p>
            </motion.div>
          ))}
        </div>
      </div>
    </section>
  );
}

Avis

Values Animated — React Values Section — Incubator