Retour au catalogue

Values Animated Counters

Valeurs illustrees par des compteurs animes et barres de progression.

valuescomplex Both Responsive a11y
bolddarkuniversalsaasgrid
Theme
"use client";
import React, { useRef } from "react";
import { motion, useInView } from "framer-motion";
import * as LucideIcons from "lucide-react";

interface ValueItem { id: string; title: string; metric: string; progress: number; icon?: string; }
interface ValuesAnimatedCountersProps { badge?: string; title?: string; values: ValueItem[]; }

function getIcon(name?: string) { if (!name) return null; return (LucideIcons as unknown as Record<string, React.ElementType>)[name] || null; }

function ProgressRing({ value }: { value: number }) {
  const ref = useRef<SVGSVGElement>(null);
  const isInView = useInView(ref, { once: true, margin: "-40px" });
  const circumference = 2 * Math.PI * 40;
  const offset = circumference - (value / 100) * circumference;

  return (
    <svg ref={ref} width="100" height="100" viewBox="0 0 100 100" className="rotate-[-90deg]">
      <circle cx="50" cy="50" r="40" fill="none" strokeWidth="6" style={{ stroke: "var(--color-border)" }} />
      <motion.circle cx="50" cy="50" r="40" fill="none" strokeWidth="6" strokeLinecap="round" style={{ stroke: "var(--color-accent)" }} strokeDasharray={circumference} initial={{ strokeDashoffset: circumference }} animate={isInView ? { strokeDashoffset: offset } : { strokeDashoffset: circumference }} transition={{ duration: 1.5, ease: [0.16, 1, 0.3, 1], delay: 0.2 }} />
    </svg>
  );
}

export default function ValuesAnimatedCounters({ badge, title, values }: ValuesAnimatedCountersProps) {
  return (
    <section className="py-[var(--section-padding-y,6rem)]" style={{ backgroundColor: "var(--color-background-dark)" }}>
      <div className="mx-auto max-w-5xl px-[var(--container-padding-x,1.5rem)]">
        <motion.div initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true, margin: "-80px" }} transition={{ duration: 0.5 }} className="text-center max-w-2xl mx-auto">
          {badge && <span className="inline-block text-xs font-medium tracking-wider uppercase px-3 py-1 rounded-full border" style={{ color: "var(--color-accent)", borderColor: "var(--color-border-dark)" }}>{badge}</span>}
          {title && <h2 className="mt-4 text-3xl font-bold tracking-tight md:text-4xl" style={{ color: "var(--color-foreground-on-dark)" }}>{title}</h2>}
        </motion.div>
        <div className="mt-14 grid grid-cols-2 gap-8 md:grid-cols-4">
          {values.map((value, i) => {
            const Icon = getIcon(value.icon);
            return (
              <motion.div key={value.id} initial={{ opacity: 0, y: 24 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true, margin: "-40px" }} transition={{ delay: i * 0.1, duration: 0.5 }} className="flex flex-col items-center text-center">
                <div className="relative">
                  <ProgressRing value={value.progress} />
                  <div className="absolute inset-0 flex items-center justify-center">
                    {Icon && <Icon className="h-6 w-6" style={{ color: "var(--color-accent)" }} />}
                  </div>
                </div>
                <span className="mt-3 text-2xl font-bold" style={{ color: "var(--color-accent)" }}>{value.metric}</span>
                <span className="mt-1 text-xs" style={{ color: "var(--color-foreground-light)" }}>{value.title}</span>
              </motion.div>
            );
          })}
        </div>
      </div>
    </section>
  );
}

Avis

Values Animated Counters — React Values Section — Incubator