Розробка мультишагової форми (Multi-Step Form) на сайті
Мультишагова форма розбиває довгий процес на послідовні кроки. Психологічно простіше заповнити 4 екрани по 3 поля, ніж один екран з 12 полями. Застосовується в онбордингу, оформленні замовлення, заявках на послуги, анкетах.
Архітектура компонента
import { useForm, FormProvider } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { useState } from 'react';
// Схеми валідації для кожного кроку
const step1Schema = z.object({
firstName: z.string().min(2, 'Мінімум 2 символи'),
lastName: z.string().min(2),
email: z.string().email('Некоректний email'),
});
const step2Schema = z.object({
phone: z.string().regex(/^\+\d{1,3}\d{6,14}$/, 'Формат: +1234567890'),
city: z.string().min(2),
address: z.string().optional(),
});
const step3Schema = z.object({
serviceId: z.number().positive(),
preferredDate: z.string(),
comment: z.string().max(500).optional(),
});
const STEPS = [step1Schema, step2Schema, step3Schema];
export function MultiStepForm({ onSubmit }: { onSubmit: (data: FormData) => Promise<void> }) {
const [currentStep, setCurrentStep] = useState(0);
const [formData, setFormData] = useState<Partial<FormData>>({});
const form = useForm({
resolver: zodResolver(STEPS[currentStep]),
defaultValues: formData,
});
async function handleNext(stepData: Partial<FormData>) {
const merged = { ...formData, ...stepData };
setFormData(merged);
if (currentStep < STEPS.length - 1) {
setCurrentStep(s => s + 1);
form.reset(merged);
} else {
await onSubmit(merged as FormData);
}
}
const progress = ((currentStep + 1) / STEPS.length) * 100;
return (
<FormProvider {...form}>
{/* Прогрес-бар */}
<div className="mb-8">
<div className="flex justify-between text-sm text-gray-500 mb-2">
<span>Крок {currentStep + 1} з {STEPS.length}</span>
<span>{Math.round(progress)}%</span>
</div>
<div className="h-2 bg-gray-100 rounded-full">
<div
className="h-2 bg-blue-500 rounded-full transition-all duration-500"
style={{ width: `${progress}%` }}
/>
</div>
</div>
{/* Кроки */}
<form onSubmit={form.handleSubmit(handleNext)}>
{currentStep === 0 && <Step1ContactInfo />}
{currentStep === 1 && <Step2DeliveryInfo />}
{currentStep === 2 && <Step3ServiceSelection />}
<div className="flex gap-3 mt-6">
{currentStep > 0 && (
<button
type="button"
onClick={() => setCurrentStep(s => s - 1)}
className="btn-secondary"
>
Назад
</button>
)}
<button type="submit" className="btn-primary flex-1">
{currentStep < STEPS.length - 1 ? 'Далі' : 'Відправити'}
</button>
</div>
</form>
</FormProvider>
);
}
Збереження прогресу
Дані кожного кроку зберігаються в localStorage — користувач може закрити вкладку та повернутися:
// Авто-збереження в localStorage
useEffect(() => {
const saved = localStorage.getItem('form_draft');
if (saved) setFormData(JSON.parse(saved));
}, []);
function handleNext(stepData) {
const merged = { ...formData, ...stepData };
setFormData(merged);
localStorage.setItem('form_draft', JSON.stringify(merged));
// ...
}
// Очищення після успішної відправки
function handleSuccess() {
localStorage.removeItem('form_draft');
}
Анімація переходів
import { AnimatePresence, motion } from 'framer-motion';
<AnimatePresence mode="wait">
<motion.div
key={currentStep}
initial={{ opacity: 0, x: 50 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -50 }}
transition={{ duration: 0.2 }}
>
{/* Вміст поточного кроку */}
</motion.div>
</AnimatePresence>
Терміни
Мультишагова форма на 3–5 кроків з валідацією та збереженням прогресу: 3–5 робочих днів.







