Your First Wizard
Build a working three-step form, then add validation and collect the data.
Your First Wizard
We will build a three-step signup form, then add validation and read the data back out. Each step below is complete and runnable.
1. The shell
Wrap FormWizard.TabContent elements in a FormWizard. Each one becomes a
step.
import FormWizard from "react-form-wizard-component";
import "react-form-wizard-component/styles.css";
export default function Signup() {
return (
<FormWizard>
<FormWizard.TabContent title="Account">
<input placeholder="Email" />
</FormWizard.TabContent>
<FormWizard.TabContent title="Profile">
<input placeholder="Full name" />
</FormWizard.TabContent>
<FormWizard.TabContent title="Review">
<p>Looks good?</p>
</FormWizard.TabContent>
</FormWizard>
);
}You already have a progress bar, Back/Next/Finish buttons, keyboard navigation, swipe support on touch devices, and screen-reader announcements.
Sample 1: Basic Wizard
Simple children API
2. Know when it finishes
onComplete fires when Finish is pressed on a valid last step.
<FormWizard onComplete={() => console.log("done")}>3. Hold the data in the wizard
Rather than wiring up your own state, let the wizard carry it. Switch to the schema API, which also unlocks conditional steps and per-step validation.
import { useRef } from "react";
import FormWizard, {
type FormWizardMethods,
type FormWizardSchema,
} from "react-form-wizard-component";
import "react-form-wizard-component/styles.css";
export default function Signup() {
const wizard = useRef<FormWizardMethods>(null);
const set = (patch: Record<string, unknown>) =>
wizard.current?.updateData(patch);
const schema: FormWizardSchema = {
initialData: { email: "", name: "" },
steps: [
{
id: "account",
title: "Account",
// `content` can be a function of the current data.
content: ({ data }) => (
<input
value={String(data.email ?? "")}
onChange={(e) => set({ email: e.target.value })}
placeholder="Email"
/>
),
},
{
id: "profile",
title: "Profile",
content: ({ data }) => (
<input
value={String(data.name ?? "")}
onChange={(e) => set({ name: e.target.value })}
placeholder="Full name"
/>
),
},
{
id: "review",
title: "Review",
content: ({ data }) => (
<p>
{String(data.name)} — {String(data.email)}
</p>
),
},
],
};
return (
<FormWizard
ref={wizard}
schema={schema}
onComplete={(data) => console.log("submitting", data)}
/>
);
}4. Stop people skipping ahead
A step's validate returns true to allow navigation, or a string to
block it and supply the message.
{
id: "account",
title: "Account",
content: ({ data }) => /* … */,
validate: ({ data }) =>
String(data.email ?? "").includes("@") ? true : "Enter a valid email",
}Next is now blocked and the step marker turns red until the email is valid.
validate runs on every render. Keep it synchronous and free of side
effects — never await, never call setState from it. For asynchronous
checks, see the pattern in the validation
guide.
Real schemas are easier with an adapter:
import { zodValidator } from "react-form-wizard-component";
import { z } from "zod";
const account = z.object({ email: z.string().email("Enter a valid email") });
validate: zodValidator(account, { pick: ["email"] });5. Hide steps that do not apply
condition removes a step entirely when it returns false — the progress bar
and step count adjust automatically.
{
id: "billing",
title: "Billing",
condition: ({ data }) => data.plan === "premium",
content: <BillingFields />,
}Where to go next
| If you want to… | Read |
|---|---|
| Validate with Zod or react-hook-form | Validation |
| Use your own markup entirely | Headless |
| Restyle or unstyle it | Theming |
| Survive a page reload | Persistence |
| See a complete build | Real-world examples |
| Look up a prop | API reference |