Skip to content
oks-ui

React Multi-Step Form with Progress Steps

Tutorial · on · by oks-ui team

A form panel with a switch, a ticked checkbox and a selected radio button stacked in rows.
ShareXLinkedIn
On this page
  1. 1. Define the steps
  2. 2. Choose a progress header
  3. 3. Check something on the server before moving on
  4. 4. Change the button labels
  5. 5. Control the step yourself
  6. 6. Your own header design
  7. Tips for good multi-step forms

Long forms convert better when they are split into short steps: account first, then profile, then preferences. The tricky parts are validating only the current step, keeping everything the user typed when they go back, and showing where they are. oks-ui's SteppedForm handles all three.

import { SteppedForm } from "oks-ui/stepped-form";
import "oks-ui/stepped-form.css";
import { FormFieldSet } from "oks-ui/form-field-set";
import "oks-ui/form-field-set.css";

1. Define the steps

Each step has a key, a title, its content, and a fields list — the names of the fields that must be valid before the user can press Next.

export function SignupWizard() {
  return (
    <SteppedForm
      onSubmit={(data) => createAccount(data)}
      steps={[
        {
          key: "account",
          title: "Account",
          fields: ["email", "password"],
          content: (
            <>
              <FormFieldSet type="email" name="email" label="Email"
                validation={{ rules: { required: true, email: true } }} />
              <FormFieldSet type="password" name="password" label="Password"
                validation={{ rules: { required: true, minLength: 8 } }} />
            </>
          ),
        },
        {
          key: "profile",
          title: "Profile",
          fields: ["fullName", "company"],
          content: (
            <>
              <FormFieldSet type="text" name="fullName" label="Full name"
                validation={{ rules: { required: true } }} />
              <FormFieldSet type="text" name="company" label="Company" />
            </>
          ),
        },
        {
          key: "preferences",
          title: "Preferences",
          isOptional: true,
          content: (
            <FormFieldSet type="switch" name="newsletter" label="Send me product updates" />
          ),
        },
      ]}
    />
  );
}

Pressing Next validates only the current step's fields. Back keeps every value, and the final Submit validates the whole form before calling onSubmit with all the data together.

2. Choose a progress header

headerVariant sets how progress is shown:

  • "dots" (default) — numbered steps with titles.
  • "progress" — a progress bar.
  • "tabs" — step titles as tabs.
  • "none" — no header, when you draw your own.
<SteppedForm headerVariant="progress" steps={steps} onSubmit={submit} />

By default users can click the header to jump back to earlier steps. disableHeaderNavigation turns that off for strictly linear flows.

Advertisement

3. Check something on the server before moving on

Some checks can't be expressed as field rules — "is this email already registered?" A step's onValidate runs after its fields pass. Return true to continue, or a string to stop with that message:

{
  key: "account",
  title: "Account",
  fields: ["email", "password"],
  content: /* …fields… */,
  onValidate: async (data) => {
    const taken = await isEmailTaken(String(data.email));
    return taken ? "That email already has an account" : true;
  },
}

While the check runs, Next shows a loading state and both buttons are disabled, so it can't be sent twice. Note that onValidate only runs on Next; do final server checks in onSubmit.

4. Change the button labels

<SteppedForm backLabel="Previous" nextLabel="Continue" submitLabel="Create account" steps={steps} onSubmit={submit} />

5. Control the step yourself

To keep the current step in the URL, or move between steps from outside the form, control it with stepIndex and onStepChange:

const [step, setStep] = useState(0);

<SteppedForm stepIndex={step} onStepChange={setStep} steps={steps} onSubmit={submit} />

6. Your own header design

For a fully custom look, renderHeader receives the steps and the active index:

<SteppedForm
  headerVariant="none"
  renderHeader={({ steps, activeStepIndex }) => (
    <p>Step {activeStepIndex + 1} of {steps.length}: {steps[activeStepIndex].title}</p>
  )}
  steps={steps}
  onSubmit={submit}
/>

Tips for good multi-step forms

  • Keep it to three to five steps, each with a clear title.
  • Put the most important fields first, so an abandoned form still collected them.
  • Mark truly optional steps with isOptional.

See a complete example on the onboarding wizard pattern.

Advertisement

oks-ui team

We build oks-ui — a React component library in strict TypeScript, themed entirely through CSS custom properties, with no runtime dependencies beyond React. These posts are what we learned building it.

Read the docs · All posts

Was this post helpful?

ShareXLinkedIn