ComponentsMulti Step Modal

Multi Step Modal

Luxe

A library of components ready for you to copy and paste, designed to illuminate your apps with elegance, sophistication and a unique touch of style.

Terminal
npm i motion react-use-measure clsx tailwind-merge
utils/cn.ts
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}
MultiStepModal.tsx
"use client"; // @NOTE: add in case you are using Next.js

import { useCallback, useState } from "react";

import { AnimatePresence, Variants, motion } from "motion/react";
import useMeasure from "react-use-measure";

import { cn } from "@/utils/cn";

const STEPS = [
  {
    title: "Luxe",
    description:
      "A library of components ready for you to copy and paste, designed to illuminate your apps with elegance, sophistication and a unique touch of style.",
  },
  {
    title: "How to use?",
    description:
      "Simply click on a component, copy the code and paste it into your project. This will give your app an extra shine.",
  },
  {
    title: "Results",
    description:
      "Luxe will add extra shine to your application, with smooth components.",
  },
  {
    title: "Copy now",
    description:
      "Elevate your project with sophisticated, ready to use components. Illuminate up your app quickly, easily and effortlessly!",
  },
];

export function MultiStepModal() {
  const [activeIdx, setActiveIdx] = useState(0);
  const [direction, setDirection] = useState(1);
  const [ref, { height: heightContent }] = useMeasure();

  const handleSetActiveIdx = useCallback(
    (idx: number) => {
      if (activeIdx < 0) setActiveIdx(0);
      if (activeIdx >= STEPS.length) setActiveIdx(STEPS.length - 1);

      const direction = idx > activeIdx ? 1 : -1;
      setDirection(direction);
      setActiveIdx(idx);
    },
    [activeIdx],
  );

  const variants: Variants = {
    initial: (direction: number) => ({
      opacity: 0,
      height: heightContent > 0 ? heightContent : "auto",
      position: "absolute",
      x: direction > 0 ? 370 : -370,
    }),
    animate: {
      opacity: 1,
      height: heightContent > 0 ? heightContent : "auto",
      position: "relative",
      x: 0,
      zIndex: 1,
    },
    exit: (direction: number) => ({
      zIndex: 0,
      opacity: 0,
      x: direction < 0 ? 370 : -370,
      top: 0,
      width: "100%",
    }),
  };

  return (
    <div className="w-[370px] overflow-hidden rounded-xl border border-[#dddddd] bg-neutral-100 dark:border-[#222222] dark:bg-[#111111]">
      <div className="relative">
        <AnimatePresence initial={false} mode="popLayout" custom={direction}>
          <motion.div
            key={activeIdx}
            custom={direction}
            variants={variants}
            initial="initial"
            animate="animate"
            exit="exit"
            transition={{
              x: { type: "spring", stiffness: 300, damping: 30 },
              opacity: { duration: 0.2 },
            }}
          >
            <div ref={ref} className="px-4 py-5">
              <h3 className="mb-2 font-medium text-neutral-700 dark:text-neutral-100">
                {STEPS[activeIdx].title}
              </h3>
              <p className="text-[15px] text-neutral-500 dark:text-neutral-400">
                {STEPS[activeIdx].description}
              </p>
            </div>
          </motion.div>
        </AnimatePresence>
        <div className="relative z-10 border-t border-[#dddddd] bg-neutral-100 dark:border-[#222222] dark:bg-[#0f0f0f]">
          <div className="flex items-center justify-between px-4 py-2">
            <button
              disabled={activeIdx === 0}
              onClick={() => handleSetActiveIdx(activeIdx - 1)}
              className={cn(
                "h-8 w-24 rounded-full border border-neutral-300 bg-neutral-100 px-3 text-[13px] font-medium text-primary",
                "disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-800 dark:bg-[#171717]",
              )}
            >
              Back
            </button>
            <button
              disabled={activeIdx === STEPS.length - 1}
              onClick={() => {
                if (activeIdx === STEPS.length - 1) return;

                handleSetActiveIdx(activeIdx + 1);
              }}
              className={cn(
                "h-8 w-24 rounded-full border border-neutral-300 bg-neutral-100 px-3 text-[13px] font-medium text-primary",
                "disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-800 dark:bg-[#171717]",
              )}
            >
              Continue
            </button>
          </div>
        </div>
      </div>
    </div>
  );
}