Multi-Step Loader
A token-aware loader that animates through a sequence of status messages using spring transitions.
Installation
Add this component to your project using the CLI:
npx vui-registry-cli-v1 add multi-step-loaderSource Code
"use client";
import { cn } from "@/lib/utils";
import { AnimatePresence, motion } from "framer-motion";
import { useState, useEffect } from "react";
// --- Elegant SVG Icons ---
const CheckIcon = () => (
<motion.svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2.5}
className="w-5 h-5 text-primary"
initial={{ pathLength: 0, scale: 0.5, opacity: 0 }}
animate={{ pathLength: 1, scale: 1, opacity: 1 }}
transition={{ duration: 0.5, ease: "easeOut" }}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</motion.svg>
);
const SpinnerIcon = () => (
<motion.div
animate={{ rotate: 360 }}
transition={{ repeat: Infinity, duration: 1.2, ease: "linear" }}
className="w-5 h-5 border-[2px] border-primary/20 border-t-primary rounded-full"
/>
);
// --- Individual Step (Strictly Aligned) ---
const StepItem = ({
text,
status,
distance,
}: {
text: string;
status: "completed" | "active" | "waiting";
distance: number;
}) => {
return (
<motion.div
initial={false}
animate={{
y: distance * 64, // Increased spacing for a more breathable, premium layout
opacity: status === "active" ? 1 : Math.max(1 - Math.abs(distance) * 0.3, 0),
scale: status === "active" ? 1 : 0.95,
filter: status === "active" ? "blur(0px)" : `blur(${Math.abs(distance) * 1.5}px)`,
}}
transition={{ type: "spring", stiffness: 200, damping: 25, mass: 0.8 }}
className="absolute inset-x-0 flex items-center justify-center w-full px-4"
>
{/* Strict Alignment Container: Locks the icon and text to a fixed width */}
<div className="w-full max-w-[280px] flex items-center justify-start gap-5">
<div className="relative flex-shrink-0 w-8 h-8 flex items-center justify-center">
<AnimatePresence mode="wait">
{status === "completed" && <CheckIcon key="check" />}
{status === "active" && <SpinnerIcon key="spinner" />}
{status === "waiting" && (
<motion.div
key="dot"
initial={{ opacity: 0, scale: 0 }}
animate={{ opacity: 1, scale: 1 }}
className="w-1.5 h-1.5 bg-foreground/20 rounded-full"
/>
)}
</AnimatePresence>
</div>
<motion.span
className={cn(
"text-[1.1rem] font-medium tracking-tight transition-all duration-300",
status === "active" ? "text-foreground drop-shadow-sm" : "text-foreground/40"
)}
>
{text}
</motion.span>
</div>
</motion.div>
);
};
// --- Core Loader ---
export const MultiStepLoader = ({
loadingStates,
loading,
duration = 2000,
}: {
loadingStates: { text: string }[];
loading?: boolean;
duration?: number;
}) => {
const [index, setIndex] = useState(0);
useEffect(() => {
if (!loading) return;
const interval = setInterval(() => {
setIndex((prev) => (prev < loadingStates.length - 1 ? prev + 1 : prev));
}, duration);
return () => clearInterval(interval);
}, [loading, loadingStates.length, duration]);
return (
<AnimatePresence>
{loading && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0, transition: { duration: 0.3 } }}
className="fixed inset-0 z-[100] flex items-center justify-center bg-background/70 backdrop-blur-2xl"
>
{/* Focal Container:
Uses a gradient mask to smoothly fade out the top and bottom items
while keeping the center perfectly clear.
*/}
<div className="relative h-[300px] w-full flex items-center justify-center [mask-image:linear-gradient(to_bottom,transparent_0%,black_30%,black_70%,transparent_100%)]">
{loadingStates.map((state, i) => (
<StepItem
key={i}
text={state.text}
distance={i - index}
status={i < index ? "completed" : i === index ? "active" : "waiting"}
/>
))}
</div>
</motion.div>
)}
</AnimatePresence>
);
};
// --- Preview Component ---
export default function VelocityUILoader() {
const [loading, setLoading] = useState(false);
// Velocity UI component library loading sequence
const loadingStates = [
{ text: "Booting Velocity engine" },
{ text: "Resolving dependencies" },
{ text: "Compiling UI components" },
{ text: "Injecting motion physics" },
{ text: "Hydrating interface" },
];
return (
<div className="flex items-center justify-center min-h-[400px] bg-card border rounded-2xl relative overflow-hidden shadow-sm p-4">
<button
onClick={() => setLoading(true)}
className="px-6 py-3 rounded-xl font-semibold z-10 bg-foreground text-background hover:scale-105 active:scale-95 transition-all shadow-md tracking-tight"
>
Install Velocity UI
</button>
<MultiStepLoader
loadingStates={loadingStates}
loading={loading}
duration={1800}
/>
{loading && (
<button
className="fixed top-8 right-8 z-[110] text-foreground/50 hover:text-foreground bg-foreground/5 backdrop-blur-md px-4 py-2 rounded-full text-xs font-semibold tracking-wider uppercase transition-all border border-border/50"
onClick={() => setLoading(false)}
>
Cancel
</button>
)}
</div>
);
}Dependencies
framer-motion: latest
Props
Component property reference.
| Name | Type | Default | Description |
|---|---|---|---|
| loadingStates | Array<{ text: string }> | [] | List of messages shown in sequence. |
| loading | boolean | false | Whether the loader is active. |
| duration | number | 2000 | Time in ms between steps. |
| loop | boolean | true | Loop through messages continuously. |
| className | string | undefined | Additional CSS classes. |
Most components here are inspired by outstanding libraries and creators in the ecosystem. I don’t claim to be the original author — this is my space for learning, rebuilding, and understanding great work at a deeper level.
I’m still a student of the craft, constantly studying the best and translating what I learn through my own perspective. Every piece reflects curiosity, respect for the community, and small creative touches that feel true to me.
I’ve done my best to credit inspirations properly. If anything is missing or inaccurate, I truly appreciate a message so it can be corrected with care.

