Building a Multi-Step Form in React with TypeScript
How to build a multi-step form with progress indicators, validation between steps, and TypeScript types that keep everything consistent.
When you need a multi-step form
Long forms kill completion rates. If your form has more than 5-6 fields, splitting it into steps makes it feel manageable. Users see 3 fields instead of 15, and the progress indicator tells them how far they've come.
Multi-step forms are common in onboarding flows, checkout processes, project inquiry forms, and survey tools. The pattern is the same regardless of the use case.
The data structure
Start by defining a TypeScript interface for your complete form data. For a project inquiry form, that might be: contact info (name, email, company), project details (type, budget, timeline), and description (message, attachments).
Each step of the form works with a subset of this interface. TypeScript ensures you don't forget a field or pass the wrong type between steps. Define a union type for the step names and a record type that maps each step to its fields.
State management
Use a single useState with the complete form data object. Each step reads and writes to the same state. This is simpler than having separate state for each step — when the user goes back, their previous answers are still there.
Track the current step with a separate useState<number>. Moving between steps is just incrementing or decrementing this number. The form data persists regardless of which step is displayed.
Validation between steps
Validate the current step's fields before allowing the user to proceed. A simple approach: create a validateStep function that takes the step number and form data, then returns either null (valid) or an error message.
Don't validate the entire form on every step — that's confusing. Only validate the fields visible in the current step. Save full validation for the final submission. This keeps error messages relevant and actionable.
The progress indicator
A progress bar or step indicator is essential. Without it, users don't know how long the form is and are more likely to abandon it. Show the step names, highlight the current step, and mark completed steps with a checkmark.
Implementation is straightforward: map over your step definitions and conditionally apply styles based on whether each step is completed, current, or upcoming. Tailwind makes this easy with conditional classes.
Handling submission
On the final step, the submit button triggers a function that sends the complete form data to your API. Use a Server Action in Next.js or a regular fetch() call to an API route. Show a loading state during submission and a success or error state after.
Consider what happens if the API call fails. Don't clear the form data — keep it intact so the user can retry. Show a clear error message with a retry button. Losing 15 fields of input to a network error is a terrible user experience.
The pattern in practice
Multi-step forms look complex but they're just a single form component with conditional rendering. The core is: a data state, a step state, a renderStep function, and forward/back navigation. TypeScript keeps the data shape honest across all steps.
We use this pattern in our project inquiry forms. It works well for collecting structured information without overwhelming the user. If you need a multi-step form built for your application — or any other complex UI component — that's exactly what we build. Start a project with us.
Need a custom version?
We build it for you.
Custom web applications, business systems, and marketing sites — built to your exact specifications. Projects starting from $2K.