WizardForm

<WizardForm> is a modal multi-step form layout. Users compose <WizardForm.Step> around admin inputs; the wizard renders inside a dialog, gates progression on per-step validation, and submits via the ra-core save pipeline (when nested inside <Create>/<Edit>) or a custom onSubmit.

Installation

pnpm dlx shadcn@latest add @shadmin/wizard-form

Usage

import { useState } from "react";
import { Button } from "@/components/ui/button";
import { WizardForm, TextInput, NumberInput } from "@/components/admin";
import { required } from "ra-core";
 
const CreateProductButton = () => {
  const [open, setOpen] = useState(false);
  return (
    <>
      <Button onClick={() => setOpen(true)}>New product</Button>
      <WizardForm
        isOpen={open}
        onClose={() => setOpen(false)}
        title="Create a product"
        description="Three quick steps."
        onSubmit={async (values) => {
          // ...
        }}
      >
        <WizardForm.Step label="Identity">
          <TextInput source="reference" validate={required()} />
          <TextInput source="name" validate={required()} />
        </WizardForm.Step>
        <WizardForm.Step label="Pricing">
          <NumberInput source="price" validate={required()} />
        </WizardForm.Step>
        <WizardForm.Step label="Review">
          <TextInput source="notes" />
        </WizardForm.Step>
      </WizardForm>
    </>
  );
};

Inside a <Create> or <Edit> view, the onSubmit prop is optional — the wizard inherits the save context and calls dataProvider.create / dataProvider.update on Save.

Props

PropRequiredTypeDefaultDescription
isOpenRequiredboolean-Controls dialog visibility.
onCloseRequired() => void-Called when the dialog requests to close. Also called by the default Cancel button after the form is reset.
titleRequiredReactNode-Dialog title.
descriptionOptionalReactNode-Dialog description rendered under the title.
childrenRequiredReactNode-One or more <WizardForm.Step> elements.
classNameOptionalstring-Forwarded to DialogContent.
progressOptional"steps" | "dots" | "none""steps"Visual progress indicator style.
toolbarOptionalReactNode | false<WizardToolbar />Custom toolbar. false hides it.
defaultValuesOptionalobject | function-Default values for the form (forwarded to <Form>).
onSubmitOptionalfunction-Submit handler. Required outside <Create>/<Edit>.
validateOptionalfunction-Form-level validation function.
recordOptionalobject-Initial record to populate the form.

<WizardForm.Step> Props

PropRequiredTypeDefaultDescription
labelRequiredstring | ReactElement-Step label shown in the progress indicator.
descriptionOptionalstring | ReactElement-Description rendered above the step's children.
optionalOptionalbooleanfalseSkip the validation gate when advancing past this step.
validateOnNextOptionalbooleantrueValidate the step's fields when Next is clicked.
classNameOptionalstring-Class applied to the step's content container.
childrenOptionalReactNode-Any admin inputs.

progress

Three modes:

  • "steps" (default) — numbered list with labels and a connector line between steps.
  • "dots" — compact dot indicator (labels are exposed to screen readers via sr-only).
  • "none" — no indicator at all.
<WizardForm isOpen={open} onClose={close} title="Create" progress="dots">
  {/* ... */}
</WizardForm>

Validation

By default every step's required fields must be valid before Next advances the wizard. The wizard uses ra-core's useFormGroups() to discover which fields belong to the current step. Mark a step optional to bypass the gate, or validateOnNext={false} for finer-grained control.

<WizardForm.Step label="Notes" optional>
  <TextInput source="notes" />
</WizardForm.Step>

If the final submit produces field errors (returned from onSubmit as { fieldName: errorMessage } per ra-core's convention, or set via <Create>/<Edit>'s save context), the wizard jumps to the first step containing an errored field. Nested errors (e.g., address.street) are matched by prefix.

toolbar

The default toolbar renders Cancel / Back / Next / Save based on the wizard's position. Cancel resets the form via form.reset() and then calls onClose() (it does NOT navigate the parent route — wizards are modal and ephemeral).

Wrap or replace the default toolbar via the toolbar prop:

import { WizardForm, WizardToolbar } from "@/components/admin";
 
<WizardForm
  isOpen={open}
  onClose={close}
  title="Create"
  toolbar={
    <div className="border-t pt-4">
      <WizardToolbar />
    </div>
  }
>
  {/* ... */}
</WizardForm>;

Set toolbar={false} to hide it entirely (useful when supplying your own buttons inside a step).

All steps stay mounted

To match <TabbedForm>, every step renders even when inactive — inactive panels are hidden with display:none and aria-hidden="true". This keeps react-hook-form's field registry intact across navigation, so values survive Back navigation and full-form validation runs on submit. Each step container also carries role="group" and data-wizard-step so styling and assistive technologies can target them.