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