Free tool
Multi-step form generator
Long forms convert better in steps. Add step breaks, name each step, and copy a multi-step form with Back / Next buttons, a progress indicator, and validation on every step. No library, no backend, and it still works as one page if JavaScript is off.
Fields
Submission
Where submissions go. Paste your form ID to make the code real — or keep the placeholder while you experiment.
Spam & privacy
Design
Adds Tailwind utility classes. Your project needs Tailwind installed.
Live preview
The preview can't run scripts, so use the step arrows above. In your copied code, Back and Next work and each step validates before moving on.
<form class="flex w-full max-w-md flex-col gap-4" action="https://shipmyform.com/f/YOUR_FORM_ID" method="POST" id="contact-form">
<p class="text-xs text-neutral-400" data-smf-progress aria-live="polite" hidden>Step 1 of 3</p>
<div class="space-y-4" data-smf-step>
<h3 class="text-base font-semibold text-neutral-200">About you</h3>
<label class="flex flex-col gap-1.5">
<span class="text-sm font-medium text-neutral-200">Name <span class="text-red-500">*</span></span>
<input class="w-full rounded-lg border px-3 py-2 text-sm outline-none border-neutral-700 bg-neutral-900 text-neutral-100 placeholder-neutral-500 focus:border-[#ffffff] focus:ring-2 focus:ring-[#ffffff]/30" type="text" name="name" placeholder="Ada Lovelace" autocomplete="name" required />
</label>
<label class="flex flex-col gap-1.5">
<span class="text-sm font-medium text-neutral-200">Email <span class="text-red-500">*</span></span>
<input class="w-full rounded-lg border px-3 py-2 text-sm outline-none border-neutral-700 bg-neutral-900 text-neutral-100 placeholder-neutral-500 focus:border-[#ffffff] focus:ring-2 focus:ring-[#ffffff]/30" type="email" name="email" placeholder="[email protected]" autocomplete="email" required />
</label>
</div>
<div class="space-y-4" data-smf-step>
<h3 class="text-base font-semibold text-neutral-200">Your project</h3>
<label class="flex flex-col gap-1.5">
<span class="text-sm font-medium text-neutral-200">What do you need? <span class="text-red-500">*</span></span>
<select class="w-full rounded-lg border px-3 py-2 text-sm outline-none border-neutral-700 bg-neutral-900 text-neutral-100 placeholder-neutral-500 focus:border-[#ffffff] focus:ring-2 focus:ring-[#ffffff]/30" name="project_type" required>
<option value="" disabled selected>Choose…</option>
<option value="New website">New website</option>
<option value="Redesign">Redesign</option>
<option value="Web app">Web app</option>
<option value="Something else">Something else</option>
</select>
</label>
<fieldset class="flex flex-col gap-2 border-0 p-0">
<legend class="mb-1 text-sm font-medium text-neutral-200">Budget</legend>
<label class="flex items-center gap-2 text-sm text-neutral-200">
<input type="radio" name="budget" value="Under $5k" />
<span>Under $5k</span>
</label>
<label class="flex items-center gap-2 text-sm text-neutral-200">
<input type="radio" name="budget" value="$5k to $20k" />
<span>$5k to $20k</span>
</label>
<label class="flex items-center gap-2 text-sm text-neutral-200">
<input type="radio" name="budget" value="Over $20k" />
<span>Over $20k</span>
</label>
</fieldset>
</div>
<div class="space-y-4" data-smf-step>
<h3 class="text-base font-semibold text-neutral-200">Details</h3>
<label class="flex flex-col gap-1.5">
<span class="text-sm font-medium text-neutral-200">Tell us more <span class="text-red-500">*</span></span>
<textarea class="w-full rounded-lg border px-3 py-2 text-sm outline-none border-neutral-700 bg-neutral-900 text-neutral-100 placeholder-neutral-500 focus:border-[#ffffff] focus:ring-2 focus:ring-[#ffffff]/30 min-h-[120px]" name="message" placeholder="Goals, timeline, links…" required></textarea>
</label>
</div>
<input type="hidden" name="_subject" value="New project request" />
<!-- Anti-spam honeypot, leave this empty -->
<input type="text" name="_gotcha" tabindex="-1" autocomplete="off" aria-hidden="true" style="position:absolute;left:-9999px" />
<div class="flex gap-2">
<button class="rounded-lg cursor-pointer border px-4 py-2.5 text-sm font-semibold border-neutral-700 text-neutral-200" type="button" data-smf-back hidden>Back</button>
<button class="rounded-lg cursor-pointer bg-[#ffffff] px-4 py-2.5 text-sm font-semibold text-black transition-opacity hover:opacity-90" type="button" data-smf-next hidden>Next</button>
<button class="rounded-lg cursor-pointer bg-[#ffffff] px-4 py-2.5 text-sm font-semibold text-black transition-opacity hover:opacity-90" type="submit" data-smf-submit>Send request</button>
</div>
</form>
<script>
(() => {
const form = document.getElementById("contact-form");
const steps = [...form.querySelectorAll("[data-smf-step]")];
const back = form.querySelector("[data-smf-back]");
const next = form.querySelector("[data-smf-next]");
const submit = form.querySelector("[data-smf-submit]");
const progress = form.querySelector("[data-smf-progress]");
const last = steps.length - 1;
let current = 0;
function show(i) {
current = i;
steps.forEach((el, n) => (el.hidden = n !== i));
back.hidden = i === 0;
next.hidden = i === last;
submit.hidden = i !== last;
progress.hidden = false;
progress.textContent = "Step " + (i + 1) + " of " + steps.length;
}
// Native validation checks the whole form, including required fields on
// steps the visitor can't see yet, which would silently block Enter. So turn
// it off and validate one visible step at a time instead.
form.noValidate = true;
// Stops at (and reports) the first problem on the current step.
function valid() {
return [...steps[current].querySelectorAll("input, select, textarea")].every(
(el) => el.reportValidity(),
);
}
next.addEventListener("click", () => valid() && show(current + 1));
back.addEventListener("click", () => show(current - 1));
form.addEventListener("submit", (e) => {
// Let the real submit through only from a valid last step. Pressing Enter
// on an earlier step moves forward instead of submitting.
if (current === last && valid()) return;
e.preventDefault();
e.stopImmediatePropagation();
if (current < last && valid()) show(current + 1);
});
show(0);
})();
</script>Steps change where people abandon, not how much you ask
Splitting a long form into steps does not reduce what you are asking for. It makes the first commitment small enough to start, and it moves the point of abandonment somewhere you can see — which is the real benefit, because an abandoned step tells you which question people refuse.
- Step one: the cheapest question
- Email before phone, role before budget. The first step exists to get someone started, so it should cost nothing to answer.
- Later steps: the work
- Detail, uploads and anything that needs thought belong after the commitment is made, not in front of it.
- A visible position
- Three of four. Without it, people cannot tell whether finishing takes ten seconds or ten minutes, and assume the worse one.
The mistake to avoid: Losing answers on Back or refresh
Nothing makes someone abandon faster than retyping what they already typed. A multi-step form has to hold its state across navigation between steps, which is the main thing that makes it harder to build than a single long form — and the thing most hand-rolled versions get wrong.
Watch where submissions stop. If everyone reaches step three and no further, step three is asking for something people will not give, and no amount of progress-bar design fixes that.
How it works
- Choose a template and fields. Pick a template, then add, remove, or reorder fields and mark the required ones. Add step breaks to turn a long form into a multi-step form.
- Style it. Pick CSS, Tailwind, or unstyled output and tune the theme: accent color, light or dark, rounded corners.
- Copy your framework's code. Switch to HTML, React, Vue, Svelte, Astro, or a fetch() version and copy, download, or open it in CodePen.
- Connect an endpoint. Create a free ShipMyForm form to get a real endpoint, then drop your form ID in so submissions actually arrive.
Want the background? Read what a form backend is and how to stop form spam without reCAPTCHA. For how the step logic works, see how to build a multi-step form in HTML. Want the submissions in your inbox? See how to send an HTML form to email. Already have a Google Form? Convert it to HTML. Got a form that submits but delivers nothing? Check its HTML.
Frequently asked questions
- How does the multi-step form work?
- It is one normal HTML form split into step sections. A short script shows one step at a time, checks the visible step's required fields before Next, and reveals Submit on the last step. Everything is submitted together in a single request, so your backend sees one ordinary submission.
- Does it need a library like a form wizard plugin?
- No. The HTML and Astro output use about 30 lines of dependency-free JavaScript. The React, Vue, and Svelte output use each framework's own state, with no extra packages.
- What happens if JavaScript is disabled?
- In the HTML and Astro output every step is visible and the Back / Next buttons stay hidden, so the form degrades to a regular single-page form with one Submit button. Nothing is lost.
- Can each step validate before moving on?
- Yes. Clicking Next runs the browser's built-in validation on the current step only and stops at the first problem. Pressing Enter on an early step moves forward instead of submitting the whole form.
- When is a multi-step form actually better?
- When the form is long enough that seeing all of it at once puts people off, and when the early questions are cheap to answer. Splitting does not reduce what you ask; it lowers the cost of starting and shows you where people stop, which a single long form never does.
- What should go on the first step?
- The least personal, least effortful question you have. Email before phone, role before budget. The first step's only job is to get someone started, so anything that makes them pause belongs after the commitment rather than in front of it.
- How do I keep answers when someone goes back a step?
- Hold the values in state rather than re-rendering empty inputs, and keep every field in a single form element so the final submit carries all of it. Losing answers on Back or a refresh is the fastest way to lose the submission, and it is the detail hand-rolled multi-step forms most often get wrong.
- Should I show a progress indicator?
- Yes. Without one, people cannot tell whether finishing takes ten seconds or ten minutes and assume the worse of the two. A plain “step 2 of 4” does the job; an animated bar adds nothing the number did not already say.