Installation
Install dependencies
npm i motion clsx tailwind-merge @tabler/icons-react
Add util file
import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
Copy the source code
components/ui/multi-step-loader.tsx
"use client";
import { cn } from "@/lib/utils";
import { AnimatePresence, motion } from "motion/react";
import { useState, useEffect } from "react";
const CheckIcon = ({ className }: { className?: string }) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className={cn("w-6 h-6 ", className)}
>
<path d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>
);
};
const CheckFilled = ({ className }: { className?: string }) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
className={cn("w-6 h-6 ", className)}
>
<path
fillRule="evenodd"
d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm13.36-1.814a.75.75 0 1 0-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.14-.094l3.75-5.25Z"
clipRule="evenodd"
/>
</svg>
);
};
type LoadingState = {
text: string;
};
const LoaderCore = ({
loadingStates,
value = 0,
}: {
loadingStates: LoadingState[];
value?: number;
}) => {
return (
<div className="flex relative justify-start max-w-xl mx-auto flex-col mt-40">
{loadingStates.map((loadingState, index) => {
const distance = Math.abs(index - value);
const opacity = Math.max(1 - distance * 0.2, 0); // Minimum opacity is 0, keep it 0.2 if you're sane.
return (
<motion.div
key={index}
className={cn("text-left flex gap-2 mb-4")}
initial={{ opacity: 0, y: -(value * 40) }}
animate={{ opacity: opacity, y: -(value * 40) }}
transition={{ duration: 0.5 }}
>
<div>
{index > value && (
<CheckIcon className="text-black dark:text-white" />
)}
{index <= value && (
<CheckFilled
className={cn(
"text-black dark:text-white",
value === index &&
"text-black dark:text-lime-500 opacity-100"
)}
/>
)}
</div>
<span
className={cn(
"text-black dark:text-white",
value === index && "text-black dark:text-lime-500 opacity-100"
)}
>
{loadingState.text}
</span>
</motion.div>
);
})}
</div>
);
};
export const MultiStepLoader = ({
loadingStates,
loading,
duration = 2000,
loop = true,
}: {
loadingStates: LoadingState[];
loading?: boolean;
duration?: number;
loop?: boolean;
}) => {
const [currentState, setCurrentState] = useState(0);
useEffect(() => {
if (!loading) {
setCurrentState(0);
return;
}
const timeout = setTimeout(() => {
setCurrentState((prevState) =>
loop
? prevState === loadingStates.length - 1
? 0
: prevState + 1
: Math.min(prevState + 1, loadingStates.length - 1)
);
}, duration);
return () => clearTimeout(timeout);
}, [currentState, loading, loop, loadingStates.length, duration]);
return (
<AnimatePresence mode="wait">
{loading && (
<motion.div
initial={{
opacity: 0,
}}
animate={{
opacity: 1,
}}
exit={{
opacity: 0,
}}
className="w-full h-full fixed inset-0 z-[100] flex items-center justify-center backdrop-blur-2xl"
>
<div className="h-96 relative">
<LoaderCore value={currentState} loadingStates={loadingStates} />
</div>
<div className="bg-gradient-to-t inset-x-0 z-20 bottom-0 bg-white dark:bg-black h-full absolute [mask-image:radial-gradient(900px_at_center,transparent_30%,white)]" />
</motion.div>
)}
</AnimatePresence>
);
};
Props
Prop Name | Type | Default Value | Description |
---|---|---|---|
loadingStates | LoadingState[] | N/A | An array of objects, each with a text property to display the current loading state message. |
loading | boolean | undefined | A boolean to control whether the loader is active or not. |
duration | number | 2000 | The duration (in milliseconds) before transitioning to the next loading state. |
loop | boolean | true | A boolean to control whether the loading states should loop back to the start. |
value | number (optional) | 0 | (Only in LoaderCore) The current index of the loading state to be displayed. |
Build websites faster and 10x better than your competitors with Aceternity UI Pro
With the best in class components and templates, stand out from the crowd and get more attention to your website. Trusted by founders and entrepreneurs from all over the world.
Manu is the man! He is the best front-end developer I have worked with.
He took the requirements and quite literally ran with them. We are super happy with the result and product we go...
John Shahawy
Founder at Rogue and Moonbeam