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