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 framer-motion react-use-measure clsx tailwind-merge
MultiStepModal.tsx
"use client"; // @NOTE: add in case you are using Next.js
import { useCallback, useState } from "react";
import { AnimatePresence, Variants, motion } from "framer-motion";
import useMeasure from "react-use-measure";
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 [activeIndex, setActiveIndex] = useState(0);
const [direction, setDirection] = useState(1);
const [ref, { height: heightContent }] = useMeasure();
const handleSetActiveIndex = useCallback(
(index: number) => {
if (activeIndex >= STEPS.length) setActiveIndex(STEPS.length - 1);
if (activeIndex < 0) setActiveIndex(0);
const newIndex = index;
const direction = newIndex > activeIndex ? 1 : -1;
setDirection(direction);
setActiveIndex(newIndex);
},
[activeIndex],
);
const variants: Variants = {
initial: (direction: number) => ({
opacity: 0,
height: heightContent > 0 ? heightContent : "auto",
x: direction > 0 ? 370 : -370,
}),
animate: {
opacity: 1,
height: heightContent > 0 ? heightContent : "auto",
x: 0,
zIndex: 1,
},
exit: (direction: number) => ({
zIndex: 0,
opacity: 0,
x: direction < 0 ? 370 : -370,
position: "absolute",
top: 0,
width: "100%",
}),
};
return (
<div className="w-[370px] overflow-hidden rounded-xl border border-[#222222] bg-[#111111]">
<div className="relative">
<AnimatePresence initial={false} mode="popLayout" custom={direction}>
<motion.div
key={activeIndex}
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-zinc-100">
{STEPS[activeIndex].title}
</h3>
<p className="text-[15px] text-neutral-400">
{STEPS[activeIndex].description}
</p>
</div>
</motion.div>
</AnimatePresence>
<div className="relative z-10 border-t border-[#222222] bg-[#0f0f0f]">
<div className="flex items-center justify-between px-4 py-2">
<button
disabled={activeIndex === 0}
onClick={() => handleSetActiveIndex(activeIndex - 1)}
className="h-8 w-24 rounded-full border border-neutral-800 bg-[#171717] px-3 text-[13px] font-medium text-primary shadow disabled:cursor-not-allowed disabled:opacity-50"
>
Back
</button>
<button
disabled={activeIndex === STEPS.length - 1}
onClick={() =>
activeIndex !== STEPS.length - 1 &&
handleSetActiveIndex(activeIndex + 1)
}
className="h-8 w-24 rounded-full border border-neutral-800 bg-[#171717] px-3 text-[13px] font-medium text-primary shadow disabled:cursor-not-allowed disabled:opacity-50"
>
Continue
</button>
</div>
</div>
</div>
</div>
);
}