Retour au catalogue

Loading Progress Steps

Indicateur de chargement multi-etapes avec barre de progression animee, icones d'etapes et messages contextuels pour chaque phase.

loading-statesmedium Both Responsive a11y
minimalcorporatesaasecommerceuniversalcentered
Theme
"use client";

import { motion } from "framer-motion";
import { Upload, Cog, Shield, Rocket, Check, Loader2 } from "lucide-react";

interface Step {
  icon: string;
  label: string;
  description: string;
}

interface LoadingProgressStepsProps {
  title?: string;
  steps?: Step[];
  currentStep?: number;
}

const EASE = [0.16, 1, 0.3, 1] as const;

const iconMap: Record<string, React.ComponentType<React.SVGProps<SVGSVGElement>>> = {
  Upload,
  Cog,
  Shield,
  Rocket,
};

export default function LoadingProgressSteps({
  title = "Preparation en cours",
  steps = [],
  currentStep = 1,
}: LoadingProgressStepsProps) {
  const progress = steps.length > 0 ? ((currentStep) / steps.length) * 100 : 0;

  return (
    <section
      style={{
        padding: "var(--section-padding-y) 0",
        background: "var(--color-background)",
      }}
    >
      <div
        style={{
          maxWidth: 600,
          margin: "0 auto",
          padding: "0 var(--container-padding-x)",
        }}
      >
        {/* Title */}
        <motion.div
          initial={{ opacity: 0, y: 12 }}
          animate={{ opacity: 1, y: 0 }}
          transition={{ duration: 0.5, ease: EASE }}
          style={{ textAlign: "center", marginBottom: "2rem" }}
        >
          <h2
            style={{
              fontFamily: "var(--font-sans)",
              fontSize: "clamp(1.5rem, 2.5vw, 2rem)",
              fontWeight: 700,
              color: "var(--color-foreground)",
              marginBottom: "0.5rem",
              letterSpacing: "-0.02em",
            }}
          >
            {title}
          </h2>

          {/* Overall progress bar */}
          <div
            style={{
              height: 4,
              borderRadius: 2,
              background: "var(--color-border)",
              overflow: "hidden",
              marginTop: "1rem",
            }}
          >
            <motion.div
              initial={{ width: "0%" }}
              animate={{ width: `${progress}%` }}
              transition={{ duration: 0.8, ease: EASE }}
              style={{
                height: "100%",
                borderRadius: 2,
                background: "var(--color-accent)",
              }}
            />
          </div>

          <p
            style={{
              fontSize: "0.8125rem",
              color: "var(--color-foreground-muted)",
              marginTop: "0.5rem",
            }}
          >
            Etape {currentStep} sur {steps.length}
          </p>
        </motion.div>

        {/* Steps */}
        <div
          style={{
            display: "flex",
            flexDirection: "column",
            gap: "0.25rem",
          }}
        >
          {steps.map((step, i) => {
            const stepNumber = i + 1;
            const isCompleted = stepNumber < currentStep;
            const isCurrent = stepNumber === currentStep;
            const isPending = stepNumber > currentStep;
            const Icon = iconMap[step.icon] ?? Cog;

            return (
              <motion.div
                key={i}
                initial={{ opacity: 0, x: -16 }}
                animate={{ opacity: 1, x: 0 }}
                transition={{
                  duration: 0.5,
                  delay: 0.1 + i * 0.1,
                  ease: EASE,
                }}
                style={{
                  display: "flex",
                  alignItems: "center",
                  gap: "1rem",
                  padding: "1rem 1.25rem",
                  borderRadius: "var(--radius-lg)",
                  background: isCurrent
                    ? "var(--color-accent-subtle)"
                    : "transparent",
                  border: isCurrent
                    ? "1px solid var(--color-accent)"
                    : "1px solid transparent",
                  opacity: isPending ? 0.5 : 1,
                }}
              >
                {/* Step indicator */}
                <div
                  style={{
                    width: 44,
                    height: 44,
                    borderRadius: "50%",
                    background: isCompleted
                      ? "var(--color-accent)"
                      : isCurrent
                        ? "var(--color-accent-subtle)"
                        : "var(--color-background-alt)",
                    border: isCompleted
                      ? "none"
                      : `2px solid ${isCurrent ? "var(--color-accent)" : "var(--color-border)"}`,
                    display: "flex",
                    alignItems: "center",
                    justifyContent: "center",
                    flexShrink: 0,
                  }}
                >
                  {isCompleted ? (
                    <Check
                      style={{
                        width: 20,
                        height: 20,
                        color: "var(--color-background)",
                      }}
                    />
                  ) : isCurrent ? (
                    <motion.div
                      animate={{ rotate: 360 }}
                      transition={{
                        duration: 1.5,
                        repeat: Infinity,
                        ease: "linear",
                      }}
                    >
                      <Loader2
                        style={{
                          width: 20,
                          height: 20,
                          color: "var(--color-accent)",
                        }}
                      />
                    </motion.div>
                  ) : (
                    <Icon
                      style={{
                        width: 20,
                        height: 20,
                        color: "var(--color-foreground-muted)",
                      }}
                    />
                  )}
                </div>

                {/* Text */}
                <div style={{ flex: 1 }}>
                  <div
                    style={{
                      fontFamily: "var(--font-sans)",
                      fontSize: "0.9375rem",
                      fontWeight: 600,
                      color: isCompleted || isCurrent
                        ? "var(--color-foreground)"
                        : "var(--color-foreground-muted)",
                      marginBottom: "0.125rem",
                    }}
                  >
                    {step.label}
                  </div>
                  <div
                    style={{
                      fontSize: "0.8125rem",
                      color: "var(--color-foreground-muted)",
                      lineHeight: 1.4,
                    }}
                  >
                    {isCompleted
                      ? "Termine"
                      : isCurrent
                        ? step.description
                        : "En attente"}
                  </div>
                </div>

                {/* Status badge */}
                {isCompleted && (
                  <span
                    style={{
                      fontSize: "0.6875rem",
                      fontWeight: 600,
                      color: "var(--color-accent)",
                      textTransform: "uppercase",
                      letterSpacing: "0.05em",
                    }}
                  >
                    Fait
                  </span>
                )}
                {isCurrent && (
                  <motion.span
                    animate={{ opacity: [1, 0.5, 1] }}
                    transition={{ duration: 1.5, repeat: Infinity }}
                    style={{
                      fontSize: "0.6875rem",
                      fontWeight: 600,
                      color: "var(--color-accent)",
                      textTransform: "uppercase",
                      letterSpacing: "0.05em",
                    }}
                  >
                    En cours
                  </motion.span>
                )}
              </motion.div>
            );
          })}
        </div>
      </div>
    </section>
  );
}

Avis

Loading Progress Steps — React Loading-states Section — Incubator