A multi-step wizard form plugin for Payload CMS that extends the official form builder with:
- Multi-step wizard navigation - Break forms into logical steps with progress indicator
- Conditional field display - Show/hide fields based on other field values
- Calculated fields - Auto-compute values from formulas
- Server-side validation - Formulas are re-computed on the server to prevent tampering
- Tailwind CSS styling - Beautiful, customizable UI with CSS variables
npm install plugin-wizard-form
# or
pnpm add plugin-wizard-form
# or
yarn add plugin-wizard-formThis plugin requires the following peer dependencies:
payload^3.0.0@payloadcms/plugin-form-builder^3.0.0react^18.0.0 || ^19.0.0react-dom^18.0.0 || ^19.0.0
// payload.config.tsimport{buildConfig}from'payload'import{wizardFormPlugin}from'@payloadcms/plugin-wizard-form'exportdefaultbuildConfig({// ... your configplugins: [wizardFormPlugin({// Optional: pass form builder optionsformBuilderOptions: {redirectRelationships: ['pages'],},}),],})- Go to Forms collection in Payload admin
- Create a new form
- Check Enable Wizard Mode in the sidebar
- Add your form fields
- For each field, expand Wizard Settings to configure:
- Step Number - Which wizard step this field belongs to (default: 1)
- Step Label - Label shown in the step indicator (set on first field of each step)
- Conditional Display - Show this field only when conditions are met
- Calculated Field - Make this a read-only computed field
// app/contact/page.tsximport{WizardForm}from'@payloadcms/plugin-wizard-form/client'import'@payloadcms/plugin-wizard-form/styles.css'exportdefaultfunctionContactPage(){return(<divclassName="max-w-2xl mx-auto py-12"><WizardFormform="your-form-id"onSuccess={()=>console.log('Form submitted!')}onError={(error)=>console.error(error)}/></div>)}Or with a pre-fetched form:
import{getPayload}from'payload'import{WizardForm}from'@payloadcms/plugin-wizard-form/client'exportdefaultasyncfunctionContactPage(){constpayload=awaitgetPayload({ config })constform=awaitpayload.findByID({collection: 'forms',id: 'your-form-id',})return<WizardFormform={form}/>}wizardFormPlugin({// Form builder options (passed through to @payloadcms/plugin-form-builder)formBuilderOptions: {redirectRelationships: ['pages'],// ... other form builder options},// Custom field blocks with wizard settingscustomBlocks: {// Your custom blocks will be extended with wizard settings},// Enable/disable server-side formula validation (default: true)enableServerValidation: true,// Additional form collection fieldsformOverrides: {fields: [// Additional fields for the Form collection],},// Additional form submission fieldsformSubmissionOverrides: {fields: [// Additional fields for the Form Submission collection],},})| Prop | Type | Default | Description |
|---|---|---|---|
form | string | WizardFormType | required | Form ID or populated form object |
hiddenFields | string[] | [] | Field names to hide |
fieldComponents | Record<string, FieldComponent> | - | Custom field components (override defaults) |
classNames | WizardClassNames | - | CSS class overrides for styling |
nextLabel | string | "Next" | Next button label |
prevLabel | string | "Previous" | Previous button label |
submitLabel | string | "Submit" | Submit button label |
submitEndpoint | string | "/api/form-submissions" | API endpoint for submissions |
onSuccess | () => void | - | Callback after successful submission |
onError | (error: Error) => void | - | Callback on submission error |
onStepChange | (step: number) => void | - | Callback when step changes |
Each form field has a Wizard Settings section with:
| Setting | Description |
|---|---|
| Step Number | Which wizard step this field appears on (default: 1) |
| Step Label | Label for the step indicator (set on first field of each step) |
Show/hide fields based on other field values:
| Option | Description |
|---|---|
| Field | Name of the field to check |
| Operator | equals, notEquals, in, notIn, exists, notExists |
| Value | Value to compare (comma-separated for in/notIn) |
Example: Show "Company Name" only when "Account Type" equals "business":
- Field:
accountType - Operator:
equals - Value:
business
Create read-only computed fields:
| Option | Description |
|---|---|
| Formula | Mathematical expression (e.g., quantity * price) |
| Output Key | Key name for the computed result |
Supported operations:
- Basic arithmetic:
+,-,*,/ - Parentheses for grouping:
(a + b) * c - Field references by name:
quantity,price - Numeric literals:
100,0.5
Override default field components with your own:
import{WizardForm}from'@payloadcms/plugin-wizard-form/client'constCustomTextInput=({ name, label, value, onChange, error, required })=>(<div><label>{label}{required&&'*'}</label><inputname={name}value={value||''}onChange={(e)=>onChange(e.target.value)}/>{error&&<spanclassName="error">{error}</span>}</div>)exportdefaultfunctionMyForm(){return(<WizardFormform="my-form-id"fieldComponents={{text: CustomTextInput,// Override other field types...}}/>)}Add the package to your Tailwind content configuration:
// tailwind.config.jsmodule.exports={content: [// ... your content'./node_modules/@payloadcms/plugin-wizard-form/dist/**/*.{js,mjs}',],}Customize colors using CSS variables:
:root {
--wizard-primary:#3b82f6;
--wizard-primary-hover:#2563eb;
--wizard-secondary:#6b7280;
--wizard-success:#22c55e;
--wizard-error:#ef4444;
--wizard-border:#e5e7eb;
--wizard-background:#ffffff;
--wizard-foreground:#111827;
--wizard-muted:#f3f4f6;
--wizard-muted-foreground:#6b7280;
}
/* Dark theme */
[data-theme="dark"] {
--wizard-primary:#60a5fa;
--wizard-background:#1f2937;
/* ... */
}Use the classNames prop for fine-grained control:
<WizardFormform="my-form"classNames={{root: 'my-custom-form',content: 'px-6 py-4',stepIndicator: 'mb-8',field: 'mb-4',navigation: 'mt-8',}}/>For custom implementations, use the useWizardForm hook:
import{useWizardForm}from'@payloadcms/plugin-wizard-form/client'functionCustomWizardForm({ fields }){const{
currentStep,
steps,
isFirstStep,
isLastStep,
formValues,
isCurrentStepValid,
setValue,
goToNext,
goToPrevious,}=useWizardForm({
fields,onStepChange: (step)=>console.log('Step:',step),})return(<div><h2>{currentStep?.label}</h2>{/* Render your custom form UI */}</div>)}All calculated fields are re-computed on the server before saving to prevent client-side tampering. The computed values are stored in a computed JSON field on the form submission.
To disable server-side validation (not recommended):
wizardFormPlugin({enableServerValidation: false,})- Email (required)
- Account Type (select: personal/business)
- Company Name (conditional: only when Account Type = business)
- Full Name (required)
- Phone Number
- Address
- Quantity (number)
- Unit Price (number)
- Total Price (calculated:
quantity * unitPrice)
// Pluginexport{wizardFormPlugin}from'@payloadcms/plugin-wizard-form'// Configexport{wizardFieldsConfig,getWizardFieldsConfig}from'@payloadcms/plugin-wizard-form'// Server-side hookexport{recomputeFormulas}from'@payloadcms/plugin-wizard-form'// Utilitiesexport{computeFormula,validateFormula,evaluateShowIf,buildSteps,}from'@payloadcms/plugin-wizard-form'// Componentsexport{WizardForm,StepIndicator,StepNavigation}from'@payloadcms/plugin-wizard-form/client'// Field componentsexport{Text,Textarea,Email,Number,Date,Select,Radio,Checkbox,Message,CalculatedField,FieldWrapper,}from'@payloadcms/plugin-wizard-form/client'// UI primitivesexport{Button,Input,Label}from'@payloadcms/plugin-wizard-form/client'// Hooksexport{useWizardForm}from'@payloadcms/plugin-wizard-form/client'MIT License - see LICENSE for details.