Retour au catalogue
Metrics Cards
Cards de metriques avec icones, valeurs et indicateurs de tendance (fleches haut/bas).
metricssimple Both Responsive a11y
minimalcorporatesaasecommerceuniversalgrid
Theme
"use client";
import { motion } from "framer-motion";
import { TrendingUp, TrendingDown, Users, DollarSign, ShoppingCart, Eye } from "lucide-react";
import type { LucideIcon } from "lucide-react";
interface MetricCard {
label: string;
value: string;
change?: string;
trend?: "up" | "down";
icon?: string;
}
interface MetricsCardsProps {
title?: string;
description?: string;
cards?: MetricCard[];
}
const EASE = [0.16, 1, 0.3, 1] as const;
const iconMap: Record<string, LucideIcon> = {
users: Users,
dollar: DollarSign,
cart: ShoppingCart,
eye: Eye,
};
export default function MetricsCards({
title = "Nos chiffres cles",
description = "Des resultats concrets et mesurables",
cards = [],
}: MetricsCardsProps) {
return (
<section
style={{
padding: "var(--section-padding-y) 0",
background: "var(--color-background)",
}}
>
<div
style={{
maxWidth: "var(--container-max-width)",
margin: "0 auto",
padding: "0 var(--container-padding-x)",
}}
>
<motion.div
initial={{ opacity: 0, y: 16 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, ease: EASE }}
style={{ textAlign: "center", marginBottom: "3rem" }}
>
<h2
style={{
fontFamily: "var(--font-sans)",
fontSize: "clamp(1.75rem, 3.5vw, 2.75rem)",
fontWeight: 700,
color: "var(--color-foreground)",
marginBottom: "0.75rem",
}}
>
{title}
</h2>
<p style={{ fontSize: "1.0625rem", color: "var(--color-foreground-muted)", maxWidth: "520px", margin: "0 auto" }}>
{description}
</p>
</motion.div>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))",
gap: "1.25rem",
}}
>
{cards.map((card, i) => {
const Icon = card.icon ? iconMap[card.icon] : null;
const TrendIcon = card.trend === "up" ? TrendingUp : TrendingDown;
const trendColor = card.trend === "up" ? "var(--color-accent)" : "var(--color-foreground-muted)";
return (
<motion.div
key={i}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.45, delay: i * 0.06, ease: EASE }}
style={{
padding: "1.5rem",
borderRadius: "var(--radius-lg)",
background: "var(--color-background-alt)",
border: "1px solid var(--color-border)",
display: "flex",
flexDirection: "column",
gap: "0.75rem",
}}
>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
<span style={{ fontSize: "0.8125rem", fontWeight: 500, color: "var(--color-foreground-muted)" }}>
{card.label}
</span>
{Icon && (
<div
style={{
width: 32,
height: 32,
borderRadius: "var(--radius-md)",
background: "var(--color-accent-subtle)",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<Icon style={{ width: 16, height: 16, color: "var(--color-accent)" }} />
</div>
)}
</div>
<span
style={{
fontFamily: "var(--font-mono, var(--font-sans))",
fontSize: "clamp(1.5rem, 2.5vw, 2rem)",
fontWeight: 700,
color: "var(--color-foreground)",
}}
>
{card.value}
</span>
{card.change && card.trend && (
<div style={{ display: "flex", alignItems: "center", gap: "0.375rem" }}>
<TrendIcon style={{ width: 14, height: 14, color: trendColor }} />
<span style={{ fontSize: "0.8125rem", fontWeight: 500, color: trendColor }}>
{card.change}
</span>
</div>
)}
</motion.div>
);
})}
</div>
</div>
</section>
);
}