How to use
SteppedForm is a linear multi-step wizard built on top of the existing Form/FormFieldSet system — it inherits every Form prop (onSubmit, validationMode, initialValues, and so on) and adds step orchestration on top: per-step field validation before advancing, four built-in header styles, and full support for async per-step checks.
import { SteppedForm, FormFieldSet } from "oks-ui";Each step needs a unique key, a title for the header, the field names it should validate before letting the user advance, and its actual content (typically a group of FormFieldSet fields). All fields across every step register with the form immediately, even on steps not yet visited — so the final submit's full-schema validation always sees the whole form, not just whichever step is currently showing.
<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: ["firstName", "lastName"],
content: (
<>
<FormFieldSet type="text" name="firstName" label="First name" validation={{ rules: { required: true } }} />
<FormFieldSet type="text" name="lastName" label="Last name" validation={{ rules: { required: true } }} />
</>
),
},
]}
/>headerVariant picks a built-in header — dots (compact, the default), progress (a filled progress bar), tabs (clickable step labels), or none if you're rendering your own step indicator elsewhere. disableHeaderNavigation stops the header itself from being a way to jump between steps, useful for a strictly linear flow.
<SteppedForm headerVariant="progress" steps={steps} onSubmit={onSubmit} />Beyond field-level validation rules, a step's onValidate runs a sync or async check before the user is allowed past it — return true to advance, or a string (or false) to block, with the string becoming the step's error message. While it's pending, Next shows a loading state and both Back/Next disable to prevent a double-submit.
onValidate only gates advancing via Next, not the final Submit — Form's own full-schema validation already covers every field on submit. If a step's async check should also block the final submission, do that inside onSubmit itself.
{
key: "account",
title: "Account",
fields: ["email"],
content: <FormFieldSet type="email" name="email" label="Email" validation={{ rules: { required: true, email: true } }} />,
onValidate: async (formData) => {
const available = await checkEmailAvailable(formData.email as string);
return available || "That email is already taken";
},
}SteppedForm stays router-agnostic on purpose — use the controlled stepIndex/onStepChange pair to sync the active step with whatever URL approach your app already uses (the History API directly, or your router's own navigation call).
const [stepIndex, setStepIndex] = useState(() => {
const n = Number(new URLSearchParams(window.location.search).get("step"));
return Number.isFinite(n) && n >= 0 ? n : 0;
});
useEffect(() => {
const params = new URLSearchParams(window.location.search);
params.set("step", String(stepIndex));
window.history.replaceState(null, "", `?${params}`);
}, [stepIndex]);
<SteppedForm stepIndex={stepIndex} onStepChange={setStepIndex} steps={steps} onSubmit={onSubmit} />All available props for SteppedForm. See the full component page for an interactive playground.
| Prop | Type | Default | Description |
|---|---|---|---|
steps | SteppedFormStep[] | — | Ordered step definitions: { key, title, description?, fields?, content, isOptional? } (required). |
backLabel / nextLabel / submitLabel | ReactNode | "Back" / "Next" / "Submit" | Footer action button labels. |
steps
SteppedFormStep[]
Default: —
Ordered step definitions: { key, title, description?, fields?, content, isOptional? } (required).
backLabel / nextLabel / submitLabel
ReactNode
Default: "Back" / "Next" / "Submit"
Footer action button labels.
| Prop | Type | Default | Description |
|---|---|---|---|
headerVariant | "dots" | "progress" | "tabs" | "none" | "dots" | Built-in step-header style. |
headerVariant
"dots" | "progress" | "tabs" | "none"
Default: "dots"
Built-in step-header style.
| Prop | Type | Default | Description |
|---|---|---|---|
initialStep | number | 0 | Uncontrolled initial step index. |
stepIndex | number | — | Controlled current step index. |
disableHeaderNavigation | boolean | false | Prevents jumping steps by clicking the header. |
renderHeader | (args) => ReactNode | — | Fully custom header renderer, replacing headerVariant. |
renderStepHeaderItem | (args) => ReactNode | — | Custom renderer for a single step-header item. |
validationMode / showErrorsOn / initialValues / ... | see Form props | see Form | All other Form props are inherited (steps replaces children). |
initialStep
number
Default: 0
Uncontrolled initial step index.
stepIndex
number
Default: —
Controlled current step index.
disableHeaderNavigation
boolean
Default: false
Prevents jumping steps by clicking the header.
renderHeader
(args) => ReactNode
Default: —
Fully custom header renderer, replacing headerVariant.
renderStepHeaderItem
(args) => ReactNode
Default: —
Custom renderer for a single step-header item.
validationMode / showErrorsOn / initialValues / ...
see Form props
Default: see Form
All other Form props are inherited (steps replaces children).
| Prop | Type | Default | Description |
|---|---|---|---|
onStepChange | (nextIndex: number) => void | — | Called when the active step changes. |
onSubmit | (formData: FormData) => void | Promise<void> | — | Inherited from Form — called on final-step submit (required). |
onStepChange
(nextIndex: number) => void
Default: —
Called when the active step changes.
onSubmit
(formData: FormData) => void | Promise<void>
Default: —
Inherited from Form — called on final-step submit (required).
| Prop | Type | Default | Description |
|---|---|---|---|
classNames | Partial<Record<SteppedFormSlot, string>> | — | Per-slot class overrides (11 slots: base/header/steps/stepButton/stepDot/stepLabel/stepDescription/progress/progressTrack/progressIndicator/content/footer/actions). |
className | string | — | Class applied to the root element. |
style | CSSProperties | — | Inline styles for the root element. |
classNames
Partial<Record<SteppedFormSlot, string>>
Default: —
Per-slot class overrides (11 slots: base/header/steps/stepButton/stepDot/stepLabel/stepDescription/progress/progressTrack/progressIndicator/content/footer/actions).
className
string
Default: —
Class applied to the root element.
style
CSSProperties
Default: —
Inline styles for the root element.