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.ts
import { buildConfig } from 'payload'
import { wizardFormPlugin } from '@payloadcms/plugin-wizard-form'
export default buildConfig({
// ... your config
plugins: [
wizardFormPlugin({
// Optional: pass form builder options
formBuilderOptions: {
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.tsx
import { WizardForm } from '@payloadcms/plugin-wizard-form/client'
import '@payloadcms/plugin-wizard-form/styles.css'
export default function ContactPage() {
return (
<div className="max-w-2xl mx-auto py-12">
<WizardForm
form="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'
export default async function ContactPage() {
const payload = await getPayload({ config })
const form = await payload.findByID({
collection: 'forms',
id: 'your-form-id',
})
return <WizardForm form={form} />
}wizardFormPlugin({
// Form builder options (passed through to @payloadcms/plugin-form-builder)
formBuilderOptions: {
redirectRelationships: ['pages'],
// ... other form builder options
},
// Custom field blocks with wizard settings
customBlocks: {
// Your custom blocks will be extended with wizard settings
},
// Enable/disable server-side formula validation (default: true)
enableServerValidation: true,
// Additional form collection fields
formOverrides: {
fields: [
// Additional fields for the Form collection
],
},
// Additional form submission fields
formSubmissionOverrides: {
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'
const CustomTextInput = ({ name, label, value, onChange, error, required }) => (
<div>
<label>{label} {required && '*'}</label>
<input
name={name}
value={value || ''}
onChange={(e) => onChange(e.target.value)}
/>
{error && <span className="error">{error}</span>}
</div>
)
export default function MyForm() {
return (
<WizardForm
form="my-form-id"
fieldComponents={{
text: CustomTextInput,
// Override other field types...
}}
/>
)
}Add the package to your Tailwind content configuration:
// tailwind.config.js
module.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:
<WizardForm
form="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'
function CustomWizardForm({ 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)
// Plugin
export { wizardFormPlugin } from '@payloadcms/plugin-wizard-form'
// Config
export { wizardFieldsConfig, getWizardFieldsConfig } from '@payloadcms/plugin-wizard-form'
// Server-side hook
export { recomputeFormulas } from '@payloadcms/plugin-wizard-form'
// Utilities
export {
computeFormula,
validateFormula,
evaluateShowIf,
buildSteps,
} from '@payloadcms/plugin-wizard-form'// Components
export { WizardForm, StepIndicator, StepNavigation } from '@payloadcms/plugin-wizard-form/client'
// Field components
export {
Text, Textarea, Email, Number, Date,
Select, Radio, Checkbox, Message,
CalculatedField, FieldWrapper,
} from '@payloadcms/plugin-wizard-form/client'
// UI primitives
export { Button, Input, Label } from '@payloadcms/plugin-wizard-form/client'
// Hooks
export { useWizardForm } from '@payloadcms/plugin-wizard-form/client'MIT License - see LICENSE for details.