React Form with Multi-step Form
A multi-step form breaks a long form into sequential pages, reducing the cognitive load for users filling out complex information. The key implementation concern is persisting data across steps — all values live in a single shared state object, and each step reads and writes to its own slice of that object. A progress bar communicates where the user is in the process.
The challenge
Multi-step forms require managing steps, progress state, per-step validation, and data persistence across steps without losing entered values.
- Persisting data across steps without Redux or any global state library
- Validating only the current step before advancing — not the entire form
- Making the back button work without losing data entered in later steps
- Showing a summary/review step with all entered data before final submission
Working code example
Here is a complete, self-contained working example you can drop directly into any React project. It uses Tailwind CSS for styling and requires no external dependencies beyond React itself.
import { useState } from 'react';
const STEPS = ['Personal Info', 'Address', 'Review'];
export default function MultiStepForm() {
const [step, setStep] = useState(0);
const [data, setData] = useState({ name: '', email: '', address: '', city: '' });
const [errors, setErrors] = useState({});
const [submitted, setSubmitted] = useState(false);
const update = (field, value) => setData((d) => ({ ...d, [field]: value }));
const validateStep = () => {
const errs = {};
if (step === 0) {
if (!data.name.trim()) errs.name = 'Name is required';
if (!data.email.trim()) errs.email = 'Email is required';
else if (!/\S+@\S+\.\S+/.test(data.email)) errs.email = 'Enter a valid email';
}
if (step === 1) {
if (!data.address.trim()) errs.address = 'Address is required';
if (!data.city.trim()) errs.city = 'City is required';
}
return errs;
};
const next = () => {
const errs = validateStep();
if (Object.keys(errs).length) { setErrors(errs); return; }
setErrors({});
setStep((s) => s + 1);
};
const back = () => { setErrors({}); setStep((s) => s - 1); };
const Field = ({ name, label, type = 'text' }) => (
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">{label}</label>
<input type={type} value={data[name]} onChange={(e) => update(name, e.target.value)}
className={`w-full border rounded-lg px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-teal-500 ${errors[name] ? 'border-red-400' : 'border-gray-300'}`} />
{errors[name] && <p className="text-red-500 text-xs mt-1">{errors[name]}</p>}
</div>
);
if (submitted)
return <p className="max-w-sm mx-auto p-6 text-green-600 font-medium">Form submitted successfully!</p>;
return (
<div className="max-w-sm mx-auto p-6 bg-white rounded-2xl shadow space-y-5">
<div className="space-y-1">
<div className="flex justify-between text-xs text-gray-500">
{STEPS.map((s, i) => (
<span key={s} className={i === step ? 'text-teal-600 font-semibold' : ''}>{s}</span>
))}
</div>
<div className="h-2 bg-gray-200 rounded-full">
<div className="h-2 bg-teal-500 rounded-full transition-all duration-300"
style={{ width: `${((step) / (STEPS.length - 1)) * 100}%` }} />
</div>
</div>
<h2 className="text-xl font-bold text-gray-800">{STEPS[step]}</h2>
{step === 0 && (
<div className="space-y-3">
<Field name="name" label="Full Name" />
<Field name="email" label="Email Address" type="email" />
</div>
)}
{step === 1 && (
<div className="space-y-3">
<Field name="address" label="Street Address" />
<Field name="city" label="City" />
</div>
)}
{step === 2 && (
<div className="space-y-2 text-sm">
{[['Name', data.name], ['Email', data.email], ['Address', data.address], ['City', data.city]].map(([label, val]) => (
<div key={label} className="flex justify-between py-1 border-b border-gray-100">
<span className="text-gray-500">{label}</span>
<span className="font-medium text-gray-800">{val}</span>
</div>
))}
</div>
)}
<div className="flex gap-3">
{step > 0 && (
<button type="button" onClick={back}
className="flex-1 border border-gray-300 text-gray-700 font-semibold py-2 rounded-lg hover:bg-gray-50 transition">
Back
</button>
)}
{step < STEPS.length - 1 ? (
<button type="button" onClick={next}
className="flex-1 bg-teal-600 text-white font-semibold py-2 rounded-lg hover:bg-teal-700 transition">
Next
</button>
) : (
<button type="button" onClick={() => setSubmitted(true)}
className="flex-1 bg-teal-600 text-white font-semibold py-2 rounded-lg hover:bg-teal-700 transition">
Submit
</button>
)}
</div>
</div>
);
}How ReactForm.co helps
ReactForm's visual builder handles all of the above — multi-step form configuration, validation rules, state management, and responsive layout — without writing a single line of code. Drag fields onto the canvas, configure their properties in the sidebar, and get production-ready React output. You can publish the form and collect responses instantly, or export the JSX to drop into your own codebase.
Build this form visuallyFrequently asked questions
How do I share state between steps in a multi-step form?
Keep all form data in a single useState object at the parent level and pass the update function down to each step. Each step reads and writes to its own keys in the shared object. When the user moves between steps, the data is preserved because the parent component does not unmount.
How do I validate only the current step before advancing?
Write a validateStep function that switches on the current step index and only checks the fields relevant to that step. Run it when the user clicks Next. If it returns errors, show them and prevent advancing. This way, the user does not see errors for fields they have not reached yet.
Should I save multi-step form data to localStorage?
It depends on how long the form takes to complete. For a quick 3-step form, localStorage is overkill. For a long form users might abandon and return to, saving to localStorage on every change and restoring on mount provides a much better experience. Serialize the data object with JSON.stringify and restore with JSON.parse.
Related topics
Build this form visually — no code needed
ReactForm.co handles multi-step form fields, validation, conditional logic, and responsive layout automatically. Publish in minutes and collect responses for free.

