How to Build Multi-Step Forms in React

Build a multi-step React form with step state, per-step validation, and accessible progress that keeps every field's value.

7 min read

Multi-step React forms split one long form into screens while keeping a single values object and a step number in state. Each step validates its own fields before the user advances, and the values persist across steps because they live in one shared object. This guide builds a three-step signup and keeps the progress accessible.

Longer forms feel less intimidating when broken into a few short screens, and each step boundary gives a clear place to validate and correct mistakes.

Track the step and the values

Two pieces of state drive the whole form: the current step number and one values object that every step writes into.

App.jsxApp.jsx
import { useState } from "react";
export default function SignupForm() {
  const [step, setStep] = useState(1);
  const [values, setValues] = useState({ name: "", email: "", plan: "" });
  const [errors, setErrors] = useState({});
 
  function handleChange(e) {
    setValues({ ...values, [e.target.name]: e.target.value });
  }
  // the return below continues this same component

Keeping the values in one object means moving from step to step never loses what the user already typed. The step number only decides which fields are visible. handleChange uses the same one-handler pattern that works for any number of fields, so every field across every step can share it.

Render one step at a time

Show the current step with a conditional render. Each field keeps the same name and value pattern as a normal controlled input, so no extra wiring is needed per step. This return statement continues the SignupForm component above.

App.jsxApp.jsx
return (
  <form onSubmit={handleSubmit}>
    {step === 1 && (
      <label>
        Name
        <input name="name" value={values.name} onChange={handleChange} />
      </label>
    )}
  </form>
);

Step one renders the name field, and steps two and three follow the same pattern with their own fields. Only the active step is in the DOM, which keeps the form short and avoids rendering every field at once.

Only visible controls exist in the DOM, so hidden steps are not submitted with the form.

Validate before advancing

Each step checks its own fields before the user can move on. A small function returns a message for the active step or null, and the Next button only advances when the step passes. Validating per step catches mistakes at the moment they are made, instead of dumping every error on the final screen.

index.jsindex.js
function validateStep(step) {
  if (step === 1 && !values.name.trim()) {
    return "Enter your name.";
  }
  if (step === 2 && !values.email.includes("@")) {
    return "Enter a valid email address.";
  }
  return null;
}

The check for step one catches an empty name, and step two catches an email without an @ sign. Step three can require a selected plan with the same idea.

App.jsxApp.jsx
function handleNext() {
  const error = validateStep(step);
  if (error) {
    setErrors({ step: error });
    return;
  }
  setErrors({});
  setStep(step + 1);
}

When the active step is valid, the error clears and the step number increases. The Back button does not validate; it just steps down. The form's onSubmit points at a handleSubmit function that only runs on the final step; it revalidates every field the same way a normal submit handler does, then sends the completed values object.

Step three can require a selected plan, and the final submit revalidates every field together before sending.

Keep the progress accessible

Show the step buttons and a message, then move focus to the new step so keyboard and screen reader users know the screen changed.

App.jsxApp.jsx
{step > 1 && <button type="button" onClick={() => setStep(step - 1)}>Back</button>}
{step < 3 && <button type="button" onClick={handleNext}>Next</button>}
{step === 3 && <button type="submit">Submit</button>}
{errors.step && <p role="alert">{errors.step}</p>}

The Back button is hidden on the first step, and the final step swaps Next for a real submit button. The alert paragraph announces any step error.

Add a visible step list above the fields and mark the active step with aria-current so screen readers announce the position. An ordered list works well, with each step as a list item and the current one labeled step.

Move focus to the new step heading after the step changes. Store a ref on the heading and call focus in the same handlers that change the step, so keyboard users are not stranded on a button that just disappeared.

Multi-step form transitions

The diagram shows the only legal moves: forward when a step validates, backward freely, and submit from the last step. The final submit still revalidates everything before sending, because client checks never replace server checks.

Keep the step number and values in one component, and split the step UI into child components when a single step grows large. The parent still owns the state and passes values plus handlers down.

For the field wiring, see how to build forms in React. For richer per-field validation, see React form validation without a library, and for the response side see how to show server validation errors in React forms.

Rune AI

Rune AI

Key Insights

  • Keep step and values in separate state.
  • Show one step with a conditional render.
  • Validate the current step before Next.
  • Keep all values in one object so steps share them.
  • Mark the active step with aria-current and move focus.
RunePowered by Rune AI

Frequently Asked Questions

How do I keep values when the user moves between steps?

Store all fields in one values object that lives above the step logic. Steps only show and hide fields, so values persist as the step number changes.

Should I validate each step or only on submit?

Validate each step before allowing Next, then validate everything again on the final submit. This gives early feedback without trusting the client for the final send.

How do I make the progress indicator accessible?

Use a real progress element or an ordered list of steps with aria-current=step on the active one, and move focus to the new step heading after the step changes.

Conclusion

A multi-step form is one values object plus a step number. Show one step at a time, validate before advancing, and keep the progress indicator accessible.