Skip to main content

How to Build a Multi-Step Form in React

Multi-step forms — also called wizard forms — split a long form into smaller, focused screens. Users fill out one section at a time, which reduces cognitive load and improves completion rates. Think checkout flows, onboarding questionnaires, or insurance applications. In this tutorial you will learn how to:
  • Build a multi-step form with forward/back navigation
  • Validate each step before the user can proceed
  • Track progress across steps
  • Handle final submission with all collected data

The problem with multi-step forms in vanilla React

Building a wizard form from scratch in React usually means managing a pile of state: the current step index, form data across steps, per-step validation, and navigation logic. Here is what that looks like:
Vanilla React wizard — a lot of wiring
const [step, setStep] = useState(0)
const [data, setData] = useState({})
const [errors, setErrors] = useState({})

const validateStep = (stepIndex) => {
  // Manual validation logic per step
  if (stepIndex === 0 && !data.name) return { name: 'Required' }
  if (stepIndex === 1 && !data.email) return { email: 'Required' }
  return {}
}

const next = () => {
  const stepErrors = validateStep(step)
  if (Object.keys(stepErrors).length > 0) {
    setErrors(stepErrors)
    return
  }
  setStep(s => s + 1)
}

// Then render different JSX per step...
This approach has several problems: validation logic is scattered, step navigation is fragile, adding a new step means touching multiple files, and there is no visual builder for non-developers on your team.

The solution: FormEngine Wizard component

FormEngine provides a Wizard component that handles all of this in JSON configuration. Each step is a WizardStep that contains its own fields and validation rules. Navigation, progress, and data collection work automatically.
wizard-form.json
{
  "form": {
    "key": "Screen",
    "type": "Screen",
    "children": [
      {
        "key": "signupWizard",
        "type": "RsWizard",
        "children": [
          {
            "key": "step1",
            "type": "RsWizardStep",
            "props": { "title": { "value": "Personal info" } },
            "children": [
              {
                "key": "fullName",
                "type": "RsInput",
                "props": { "label": { "value": "Full name" } },
                "schema": {
                  "validations": [
                    { "key": "required", "args": { "message": "Name is required" } }
                  ]
                }
              },
              {
                "key": "email",
                "type": "RsInput",
                "props": { "label": { "value": "Email" } },
                "schema": {
                  "validations": [
                    { "key": "required", "args": { "message": "Email is required" } },
                    { "key": "email", "args": { "message": "Enter a valid email" } }
                  ]
                }
              }
            ]
          },
          {
            "key": "step2",
            "type": "RsWizardStep",
            "props": { "title": { "value": "Company details" } },
            "children": [
              {
                "key": "company",
                "type": "RsInput",
                "props": { "label": { "value": "Company name" } }
              },
              {
                "key": "role",
                "type": "RsDropdown",
                "props": {
                  "label": { "value": "Your role" },
                  "data": {
                    "value": [
                      { "value": "developer", "label": "Developer" },
                      { "value": "manager", "label": "Product Manager" },
                      { "value": "designer", "label": "Designer" },
                      { "value": "other", "label": "Other" }
                    ]
                  }
                }
              }
            ]
          },
          {
            "key": "step3",
            "type": "RsWizardStep",
            "props": { "title": { "value": "Preferences" } },
            "children": [
              {
                "key": "framework",
                "type": "RsRadioGroup",
                "props": {
                  "label": { "value": "Preferred framework" },
                  "items": {
                    "value": [
                      { "value": "nextjs", "label": "Next.js" },
                      { "value": "remix", "label": "Remix" },
                      { "value": "vite", "label": "Vite + React" }
                    ]
                  }
                }
              },
              {
                "key": "newsletter",
                "type": "RsToggle",
                "props": { "label": { "value": "Subscribe to newsletter" } }
              }
            ]
          }
        ]
      }
    ]
  },
  "errorType": "RsErrorMessage"
}

Rendering the wizard

SignupWizard.tsx
import { useCallback, useRef } from 'react'
import { FormViewer, IFormViewer } from '@react-form-builder/core'
import { rsView, formEngineRsuiteCssLoader } from '@react-form-builder/components-rsuite'
import wizardForm from './wizard-form.json'

formEngineRsuiteCssLoader()

export default function SignupWizard() {
  const viewerRef = useRef<IFormViewer>(null)
  const getForm = useCallback(() => JSON.stringify(wizardForm), [])

  const handleDataChange = useCallback((formData) => {
    console.log('Current data:', formData.data)
  }, [])

  return (
    <FormViewer
      view={rsView}
      getForm={getForm}
      viewerRef={viewerRef}
      onFormDataChange={handleDataChange}
    />
  )
}
The Wizard component renders step indicators, Next/Previous buttons, and validates each step before allowing navigation. No extra wiring required.

Adding per-step validation

In the JSON above, validation rules are already defined on individual fields (the schema.validations array). The Wizard component respects these rules: when a user clicks “Next”, FormEngine validates all fields in the current step. If any field fails, the step shows errors and blocks navigation. For cross-field validation (such as “confirm email must match email”), see the validation guide.

Handling final submission

When the user reaches the last step and clicks “Finish”, you retrieve all collected data through the viewerRef:
const handleFinish = useCallback(() => {
  const viewer = viewerRef.current
  if (!viewer) return

  const { data, errors } = viewer.getFormData()
  if (Object.keys(errors).length > 0) return

  // data contains all fields from all steps:
  // { fullName: "...", email: "...", company: "...", role: "...", framework: "...", newsletter: true }
  fetch('/api/signup', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data),
  })
}, [])

Creating the same form visually

If you prefer a visual approach, you can build this exact wizard in the Online Form Builder:
  1. Drag a Wizard component onto the canvas
  2. Add WizardStep components inside it
  3. Drag form fields into each step
  4. Configure validation rules in the property panel
  5. Click “Export JSON” and paste the result into your React app
This is especially useful for non-developers on your team who need to create or modify forms without touching code. The Designer is a commercial product that you embed into your own application.

Comparison: FormEngine vs building from scratch

CapabilityVanilla ReactFormEngine
Step navigationManual state managementBuilt-in
Per-step validationCustom logic per stepAutomatic from JSON schema
Progress indicatorBuild your ownBuilt-in component
Add/remove stepsEdit multiple filesEdit JSON or drag-and-drop
Visual builder for non-devsNot availableDesigner (commercial)
Portable across appsNoSingle JSON file

FAQ

Yes. The Wizard component inherits styles from your chosen component library (RSuite, MUI, Mantine, or your own). You can also apply custom CSS styles per component or per screen size.
Yes. Use conditional rendering with renderWhen on WizardStep components. A step whose condition evaluates to false is hidden and skipped in navigation.
Pass an initialData prop to FormViewer with an object mapping field keys to values. See handling form data for details.

Next steps

Validation guide

Add Zod-based validation, async validators, and cross-field rules.

Conditional logic

Show/hide fields and steps based on user input.

Dynamic form fields

Let users add and remove repeating sections.

FormEngine vs React Hook Form

Detailed feature comparison for evaluating alternatives.