Skip to Content
ExamplesMulti-Step Form

Multi-Step Form

Long forms cause drop-off. Splitting them into steps keeps users focused on one group of questions at a time. This recipe shows a two-step React form that submits all data in a single Formtorch request at the end.

Plan your steps

Decide which fields belong to each step. A good rule of thumb: group by topic, keep each step to 3–5 fields.

For this example:

  • Step 1: Name and email
  • Step 2: Project details and budget

Build the multi-step component

components/MultiStepForm.tsx
"use client"; import { useState } from "react"; type FormData = { name: string; email: string; projectType: string; budget: string; message: string; }; const initialData: FormData = { name: "", email: "", projectType: "", budget: "", message: "", }; export function MultiStepForm() { const [step, setStep] = useState(1); const [formData, setFormData] = useState<FormData>(initialData); const [status, setStatus] = useState< "idle" | "loading" | "success" | "error" >("idle"); function update(field: keyof FormData, value: string) { setFormData((prev) => ({ ...prev, [field]: value })); } function next(e: React.FormEvent) { e.preventDefault(); setStep(2); } async function submit(e: React.FormEvent) { e.preventDefault(); setStatus("loading"); const body = new FormData(); for (const [key, value] of Object.entries(formData)) { body.append(key, value); } const res = await fetch("https://formtorch.com/f/YOUR_FORM_ID", { method: "POST", headers: { "X-Requested-With": "XMLHttpRequest" }, body, }); setStatus(res.ok ? "success" : "error"); } if (status === "success") { return ( <div> <h2>Request received</h2> <p>We'll review your details and follow up within 2 business days.</p> </div> ); } return ( <div> {/* Progress indicator */} <p>Step {step} of 2</p> {step === 1 && ( <form onSubmit={next}> <div> <label htmlFor="name">Name</label> <input id="name" type="text" value={formData.name} onChange={(e) => update("name", e.target.value)} required /> </div> <div> <label htmlFor="email">Email</label> <input id="email" type="email" value={formData.email} onChange={(e) => update("email", e.target.value)} required /> </div> <button type="submit">Next</button> </form> )} {step === 2 && ( <form onSubmit={submit}> <div> <label htmlFor="projectType">Project type</label> <select id="projectType" value={formData.projectType} onChange={(e) => update("projectType", e.target.value)} required > <option value="">Select one</option> <option value="website">Website</option> <option value="mobile-app">Mobile app</option> <option value="other">Other</option> </select> </div> <div> <label htmlFor="budget">Budget range</label> <select id="budget" value={formData.budget} onChange={(e) => update("budget", e.target.value)} required > <option value="">Select one</option> <option value="under-5k">Under $5k</option> <option value="5k-20k">$5k – $20k</option> <option value="20k-plus">$20k+</option> </select> </div> <div> <label htmlFor="message">Additional details</label> <textarea id="message" value={formData.message} onChange={(e) => update("message", e.target.value)} rows={4} /> </div> {status === "error" && ( <p role="alert">Something went wrong. Please try again.</p> )} <button type="button" onClick={() => setStep(1)}> Back </button> <button type="submit" disabled={status === "loading"}> {status === "loading" ? "Submitting…" : "Submit request"} </button> </form> )} </div> ); }

Use the component

app/quote/page.tsx
import { MultiStepForm } from "@/components/MultiStepForm"; export default function QuotePage() { return ( <main> <h1>Request a quote</h1> <MultiStepForm /> </main> ); }

Key points

  • All field data is held in React state, not in separate forms.
  • Only one fetch call happens: at the final step on submit.
  • The Back button reverts to step 1 without losing entered data.
  • Native form validation (required) still works per-step because each step has its own <form> element.

For more than 2 steps, extract the step-rendering logic into a steps array and iterate over it. The submit-at-the-end pattern stays the same regardless of how many steps you add.

Last updated on