ApplicationMulti-Step Form
Multi-Step Form
A customizable multi-step form component with progress indicator and animations.
Preview
1
Personal Info2
Address3
ConfirmationPersonal Information
Installation
Before using the MultiStepForm
component, install the necessary dependency:
npm install canvas-confetti
npm i --save-dev @types/canvas-confetti
Code
"use client";
import React, { useState, useCallback } from "react";
import { motion, AnimatePresence } from "motion/react";
import { ChevronLeft, ChevronRight } from "lucide-react";
import confetti from "canvas-confetti";
export interface StepProp {
title: string;
component: React.ReactNode;
}
export interface MultiStepFormProps {
steps: StepProp[];
onComplete: (data: any) => void;
className?: string;
}
export const MultiStepForm: React.FC<MultiStepFormProps> = ({
steps,
onComplete,
className = "",
}) => {
const [currentStep, setCurrentStep] = useState(0);
const [formData, setFormData] = useState<any>({});
const [direction, setDirection] = useState(0);
const [isCompleted, setIsCompleted] = useState(false);
const isStepValid = useCallback(() => {
const currentStepData = steps[currentStep];
switch (currentStepData.title) {
case "Personal Info":
return formData.name?.trim() && formData.email?.trim();
case "Address":
return formData.street?.trim() && formData.city?.trim();
case "Confirmation":
return true;
default:
return false;
}
}, [currentStep, formData, steps]);
const handleNext = useCallback(() => {
if (currentStep < steps.length - 1 && isStepValid()) {
setDirection(1);
setCurrentStep(currentStep + 1);
} else if (currentStep === steps.length - 1 && isStepValid()) {
setIsCompleted(true);
onComplete(formData);
confetti({
particleCount: 100,
spread: 70,
origin: { y: 0.6 },
});
}
}, [currentStep, steps.length, onComplete, formData, isStepValid]);
const handlePrevious = useCallback(() => {
if (currentStep > 0) {
setDirection(-1);
setCurrentStep(currentStep - 1);
}
}, [currentStep]);
const updateFormData = useCallback((stepData: any) => {
setFormData((prevData: any) => ({ ...prevData, ...stepData }));
}, []);
if (isCompleted) {
return (
<div className="flex items-center justify-center h-full">
<h2 className="text-2xl font-bold text-primary">
Thank you for submitting!
</h2>
</div>
);
}
return (
<div
className={`bg-secondary/50 rounded-lg border border-primary/10 p-4 ${className}`}
>
<div className="mb-6 sm:mb-8">
<div className="flex flex-wrap justify-between gap-4 items-center">
{steps.map((step, index) => (
<React.Fragment key={index}>
<div
className={`flex items-center mb-2 sm:mb-0 ${
index <= currentStep
? "text-primary"
: "text-muted-foreground"
}`}
>
<div
className={`w-6 h-6 text-sm rounded-full flex items-center justify-center border-2 ${
index < currentStep
? "bg-primary text-background"
: index === currentStep
? "border-primary"
: "border-muted-foreground"
}`}
>
{index < currentStep ? "✓" : index + 1}
</div>
<span className="ml-2 text-sm sm:text-base font-medium">
{step.title}
</span>
</div>
{index < steps.length - 1 && (
<div
className={`hidden sm:block flex-1 h-1 rounded-full mx-2 ${
index < currentStep ? "bg-primary" : "bg-primary/50"
}`}
/>
)}
</React.Fragment>
))}
</div>
</div>
<AnimatePresence mode="wait" initial={false}>
<motion.div
key={currentStep}
initial={{ opacity: 0, x: direction > 0 ? 50 : -50 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: direction > 0 ? -50 : 50 }}
transition={{ duration: 0.3 }}
>
{React.cloneElement(
steps[currentStep].component as React.ReactElement,
{
updateFormData,
formData,
},
)}
</motion.div>
</AnimatePresence>
<div className="mt-6 sm:mt-8 flex justify-between">
<button
onClick={handlePrevious}
disabled={currentStep === 0}
className="flex items-center px-3 py-1 sm:px-4 sm:py-2 bg-secondary text-secondary-foreground rounded-md disabled:opacity-50 text-sm sm:text-base"
>
<ChevronLeft className="mr-1 sm:mr-2" size={16} />
Previous
</button>
<button
onClick={handleNext}
disabled={!isStepValid()}
className="flex items-center px-3 py-1 sm:px-4 sm:py-2 bg-primary text-primary-foreground rounded-md disabled:opacity-50 text-sm sm:text-base"
>
{currentStep === steps.length - 1 ? "Complete" : "Next"}
<ChevronRight className="ml-1 sm:ml-2" size={16} />
</button>
</div>
</div>
);
};
Usage
Basic Setup
To use the MultiStepForm
component:
- Define your form steps as an array of objects containing a
title
and acomponent
. - Pass the
steps
array and a completion handler (onComplete
) to theMultiStepForm
component.
import { MultiStepForm } from "@/components/library/application/MultiStepForm";
export const MultiStepFormExample: React.FC = () => {
const [formData, setFormData] = useState<any>({});
const updateFormData = (stepData: any) => {
setFormData({ ...formData, ...stepData });
};
const steps = [
{
title: "Personal Info",
component: (
<PersonalInfoStep updateFormData={updateFormData} formData={formData} />
),
},
{
title: "Address",
component: (
<AddressStep updateFormData={updateFormData} formData={formData} />
),
},
{
title: "Confirmation",
component: (
<ConfirmationStep updateFormData={updateFormData} formData={formData} />
),
},
];
const handleComplete = (data: any) => {
console.log("Form submitted with data:", data);
};
return (
<div className="max-w-2xl mx-auto w-full px-4 sm:px-0">
<MultiStepForm steps={steps} onComplete={handleComplete} />
</div>
);
};
Step Components
Each step in the form is defined as a React component. These components should handle their own validation and communicate data back to the parent MultiStepForm
through the updateFormData
callback.
Example Steps
Personal Info Step
const PersonalInfoStep: React.FC<StepProps> = ({
updateFormData,
formData,
}) => {
const [errors, setErrors] = useState({ name: "", email: "" });
const validateField = (name: string, value: string) => {
if (!value.trim()) {
setErrors((prev) => ({ ...prev, [name]: `${name} is required` }));
} else {
setErrors((prev) => ({ ...prev, [name]: "" }));
}
updateFormData({ [name]: value });
};
return (
<div className="space-y-4">
<h2 className="text-2xl font-bold mb-4">Personal Information</h2>
<div>
<label htmlFor="name" className="block text-sm font-medium mb-1">
Name
</label>
<input
type="text"
id="name"
value={formData.name || ""}
onChange={(e) => validateField("name", e.target.value)}
className={`input ${errors.name ? "input-error" : ""}`}
/>
{errors.name && (
<p className="text-red-500 text-sm mt-1">{errors.name}</p>
)}
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium mb-1">
Email
</label>
<input
type="email"
id="email"
value={formData.email || ""}
onChange={(e) => validateField("email", e.target.value)}
className={`input ${errors.email ? "input-error" : ""}`}
/>
{errors.email && (
<p className="text-red-500 text-sm mt-1">{errors.email}</p>
)}
</div>
</div>
);
};
Address Step
Similar to the PersonalInfoStep
, this step collects address details.
Confirmation Step
This step displays a summary of the user's input.
const ConfirmationStep: React.FC<StepProps> = ({ formData }) => (
<div>
<h2 className="text-2xl font-bold mb-4">Confirmation</h2>
<p>Please confirm your information:</p>
<div className="bg-muted p-4 rounded-md">
<p>Name: {formData.name}</p>
<p>Email: {formData.email}</p>
<p>Street: {formData.street}</p>
<p>City: {formData.city}</p>
</div>
</div>
);
Props
Multi Step Form
Prop | Type | Default |
---|---|---|
steps | StepProp[] | - |
onComplete | (data: any) => void | - |
className | string | - |
Step Props
Prop | Type | Default |
---|---|---|
title | string | - |
component | ReactElement<StepProps, string | JSXElementConstructor<any>> | - |
Example Output
After completing all steps, the onComplete
function is called with the collected data:
{
name: "John Doe",
email: "johndoe@example.com",
street: "123 Main St",
city: "New York"
}
Edit on GitHub
Last updated on