Retour au catalogue

Animated Bars

Barres horizontales animees comparant deux options, croissance au scroll. Visuel et impactant.

comparisonmedium Both Responsive a11y
boldcorporatesaasuniversalstacked
Theme
"use client";

import { motion } from "framer-motion";
import { TrendingUp } from "lucide-react";

interface Metric {
  label: string;
  valueA: number;
  valueB: number;
  unit: string;
}

interface Option {
  name: string;
  label: string;
}

interface ComparisonAnimatedBarsProps {
  title?: string;
  subtitle?: string;
  optionA?: Option;
  optionB?: Option;
  metrics?: Metric[];
}

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

export default function ComparisonAnimatedBars({
  title = "Comparaison",
  subtitle = "",
  optionA = { name: "Option A", label: "A" },
  optionB = { name: "Option B", label: "B" },
  metrics = [],
}: ComparisonAnimatedBarsProps) {
  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: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ duration: 0.6, ease: E }} style={{ textAlign: "center", marginBottom: "3rem" }}>
          <h2 style={{ fontFamily: "var(--font-serif)", fontSize: "clamp(1.75rem, 3vw, 2.5rem)", fontWeight: 600, color: "var(--color-foreground)", marginBottom: "0.75rem" }}>{title}</h2>
          {subtitle && <p style={{ fontSize: "1rem", color: "var(--color-foreground-muted)", fontFamily: "var(--font-sans)" }}>{subtitle}</p>}
        </motion.div>

        {/* Legend */}
        <div style={{ display: "flex", justifyContent: "center", gap: "2rem", marginBottom: "2rem" }}>
          <div style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
            <div style={{ width: "12px", height: "12px", borderRadius: "var(--radius-full)", background: "var(--color-accent)" }} />
            <span style={{ fontSize: "0.875rem", fontWeight: 600, color: "var(--color-foreground)", fontFamily: "var(--font-sans)" }}>{optionA.name}</span>
          </div>
          <div style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
            <div style={{ width: "12px", height: "12px", borderRadius: "var(--radius-full)", background: "var(--color-border)" }} />
            <span style={{ fontSize: "0.875rem", fontWeight: 500, color: "var(--color-foreground-muted)", fontFamily: "var(--font-sans)" }}>{optionB.name}</span>
          </div>
        </div>

        {/* Bars */}
        <div style={{ maxWidth: "720px", margin: "0 auto", display: "flex", flexDirection: "column", gap: "1.5rem" }}>
          {metrics.map((m, i) => (
            <motion.div key={m.label} initial={{ opacity: 0, y: 12 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ duration: 0.5, delay: i * 0.08, ease: E }}>
              <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: "0.5rem" }}>
                <span style={{ fontSize: "0.875rem", fontWeight: 500, color: "var(--color-foreground)", fontFamily: "var(--font-sans)" }}>{m.label}</span>
                <div style={{ display: "flex", alignItems: "center", gap: "0.375rem" }}>
                  <TrendingUp size={14} style={{ color: "var(--color-accent)" }} />
                  <span style={{ fontSize: "0.8125rem", fontWeight: 700, color: "var(--color-accent)", fontFamily: "var(--font-sans)" }}>{m.valueA}{m.unit}</span>
                </div>
              </div>
              {/* Option A bar */}
              <div style={{ position: "relative", height: "28px", borderRadius: "var(--radius-full)", background: "var(--color-background-alt)", overflow: "hidden", marginBottom: "0.375rem" }}>
                <motion.div
                  initial={{ width: 0 }}
                  whileInView={{ width: `${m.valueA}%` }}
                  viewport={{ once: true }}
                  transition={{ duration: 1, delay: 0.2 + i * 0.1, ease: E }}
                  style={{ height: "100%", borderRadius: "var(--radius-full)", background: "var(--color-accent)", display: "flex", alignItems: "center", justifyContent: "flex-end", paddingRight: "0.75rem" }}
                >
                  <span style={{ fontSize: "0.6875rem", fontWeight: 700, color: "var(--color-background)", fontFamily: "var(--font-sans)" }}>{optionA.label}</span>
                </motion.div>
              </div>
              {/* Option B bar */}
              <div style={{ position: "relative", height: "20px", borderRadius: "var(--radius-full)", background: "var(--color-background-alt)", overflow: "hidden" }}>
                <motion.div
                  initial={{ width: 0 }}
                  whileInView={{ width: `${m.valueB}%` }}
                  viewport={{ once: true }}
                  transition={{ duration: 1, delay: 0.3 + i * 0.1, ease: E }}
                  style={{ height: "100%", borderRadius: "var(--radius-full)", background: "var(--color-border)", display: "flex", alignItems: "center", justifyContent: "flex-end", paddingRight: "0.75rem" }}
                >
                  <span style={{ fontSize: "0.625rem", fontWeight: 600, color: "var(--color-foreground-muted)", fontFamily: "var(--font-sans)" }}>{m.valueB}{m.unit}</span>
                </motion.div>
              </div>
            </motion.div>
          ))}
        </div>
      </div>
    </section>
  );
}

Avis

Animated Bars — React Comparison Section — Incubator