Docs

Multi Step Loader

A step loader for screens that take a lot of time to load.

Installation

Install dependencies

npm install framer-motion lucide-react

Update tailwind.config.js

Add the following to your tailwind.config.js file.

module.exports = {
  theme: {
    extend: {
    }
  }
}

Run the following command

It will create a new file multi-step-loader.tsx inside the components/mage-ui/progress directory.

mkdir -p components/mage-ui/progress && touch components/mage-ui/progress/multi-step-loader.tsx

Paste the code

Open the newly created file and paste the following code:

"use client";
import React, { useState, useEffect } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { IconSquareRoundedX } from "@tabler/icons-react";
 
const CheckIcon = ({ className }: { className?: string }) => (
  <svg
    xmlns="http://www.w3.org/2000/svg"
    fill="none"
    viewBox="0 0 24 24"
    strokeWidth={1.5}
    stroke="currentColor"
    className={`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 }) => (
  <svg
    xmlns="http://www.w3.org/2000/svg"
    viewBox="0 0 24 24"
    fill="currentColor"
    className={`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;
}) => (
  <div className="flex relative justify-start max-w-xl mx-auto flex-col mt-40">
    {loadingStates.map((loadingState, index) => {
      const opacity = Math.max(1 - Math.abs(index - value) * 0.2, 0);
      return (
        <motion.div
          key={index}
          className="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" />
            ) : (
              <CheckFilled
                className={`text-black dark:text-white ${value === index ? "dark:text-lime-500" : ""
                  }`}
              />
            )}
          </div>
          <span
            className={`text-black dark:text-white ${value === index ? "dark:text-lime-500" : ""
              }`}
          >
            {loadingState.text}
          </span>
        </motion.div>
      );
    })}
  </div>
);
 
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((prev) =>
        loop
          ? prev === loadingStates.length - 1
            ? 0
            : prev + 1
          : Math.min(prev + 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>
  );
};
 
export default function MultiStepLoaderPage() {
  const [loading, setLoading] = useState(false);
  const loadingStates: LoadingState[] = [
    { text: "Buying a condo" },
    { text: "Travelling in a flight" },
    { text: "Meeting Tyler Durden" },
    { text: "He makes soap" },
    { text: "We go to a bar" },
    { text: "Start a fight" },
    { text: "We like it" },
    { text: "Welcome to F**** C***" },
  ];
 
  return (
    <div className="w-full h-screen flex items-center justify-center">
      <MultiStepLoader loadingStates={loadingStates} loading={loading} duration={2000} />
      <button
        onClick={() => setLoading(true)}
        className="bg-[#39C3EF] hover:bg-[#39C3EF]/90 text-black mx-auto text-sm md:text-base transition font-medium duration-200 h-10 rounded-lg px-8 flex items-center justify-center"
      >
        Click to load
      </button>
      {loading && (
        <button className="fixed top-4 right-4 text-black dark:text-white z-[120]" onClick={() => setLoading(false)}>
          <IconSquareRoundedX className="h-10 w-10" />
        </button>
      )}
    </div>
  );
}