Multi-Step Forms
Create multi-step wizards by defining multiple steps with .addStep(). Each step groups a subset of fields and can route to different next steps based on field values.
Basic Multi-Step
import { FormBuilder } from '@saastro/forms';
const config = FormBuilder.create('onboarding')
.layout('manual')
.columns(12)
// Fields
.addField('name', (f) => f.type('text').label('Name').required().columns({ default: 12, md: 6 }))
.addField('email', (f) =>
f.type('email').label('Email').required().email().columns({ default: 12, md: 6 }),
)
.addField('plan', (f) =>
f
.type('select')
.label('Plan')
.required()
.options([
{ value: 'free', label: 'Free' },
{ value: 'pro', label: 'Pro' },
{ value: 'enterprise', label: 'Enterprise' },
]),
)
.addField('terms', (f) => f.type('checkbox').label('I accept the terms').mustBeTrue())
// Steps
.addStep('personal', ['name', 'email'], { defaultNext: 'plan-select' })
.addStep('plan-select', ['plan'], { defaultNext: 'confirm' })
.addStep('confirm', ['terms'])
.initialStep('personal')
// Buttons
.buttons({
align: 'between',
submit: { type: 'submit', label: 'Complete' },
next: { type: 'next', label: 'Continue' },
back: { type: 'back', label: 'Back', variant: 'outline' },
})
.build();
Key points:
defaultNextsets the next step when no conditions match- The last step has no
defaultNext— the submit button appears instead of next .initialStep()sets the starting step (defaults to the first step added)
Step Navigation
Navigation is handled automatically by the next and back buttons.
How It Works
- Next: Validates current step fields, then navigates to the next step
- Back: Returns to the previous step via the history stack (no re-validation)
- Submit: Only shown on the last step (where there’s no
defaultNext)
Step History
The form maintains a history stack. When the user goes back, they return to the exact step they came from — even with conditional routing.
Step 1 → Step 3 → Step 5 (history: [1, 3])
↑ Back goes to 3, then to 1
Conditional Routing
Route to different steps based on field values using the next option:
.addStep('inquiry-type', ['type'], {
defaultNext: 'general-info',
next: [
{
conditions: [{ field: 'type', operator: 'equals', value: 'sales' }],
operator: 'AND',
target: 'sales-info',
},
{
conditions: [{ field: 'type', operator: 'equals', value: 'support' }],
operator: 'AND',
target: 'support-info',
},
],
})
Each entry in next is a ConditionGroup with a target step ID. Conditions are evaluated in order — the first match wins. If none match, defaultNext is used.
Condition Operators
| Operator | Description | Example |
|---|---|---|
equals | Exact match | { field: 'plan', operator: 'equals', value: 'pro' } |
notEquals | Not equal | { field: 'plan', operator: 'notEquals', value: 'free' } |
contains | String/array contains | { field: 'tags', operator: 'contains', value: 'urgent' } |
notContains | Does not contain | { field: 'name', operator: 'notContains', value: 'test' } |
greaterThan | Number greater than | { field: 'age', operator: 'greaterThan', value: 18 } |
lessThan | Number less than | { field: 'budget', operator: 'lessThan', value: 1000 } |
greaterThanOrEqual | Number greater than or equal | { field: 'score', operator: 'greaterThanOrEqual', value: 80 } |
lessThanOrEqual | Number less than or equal | { field: 'quantity', operator: 'lessThanOrEqual', value: 10 } |
isTrue | Value is true | { field: 'premium', operator: 'isTrue' } |
isFalse | Value is false | { field: 'newsletter', operator: 'isFalse' } |
isEmpty | Empty, null, or undefined | { field: 'phone', operator: 'isEmpty' } |
isNotEmpty | Has a value | { field: 'company', operator: 'isNotEmpty' } |
AND / OR Grouping
Combine multiple conditions:
// AND: all conditions must be true
{
conditions: [
{ field: 'plan', operator: 'equals', value: 'enterprise' },
{ field: 'employees', operator: 'greaterThan', value: 100 },
],
operator: 'AND',
target: 'enterprise-setup',
}
// OR: any condition can be true
{
conditions: [
{ field: 'country', operator: 'equals', value: 'US' },
{ field: 'country', operator: 'equals', value: 'CA' },
],
operator: 'OR',
target: 'north-america-setup',
}
Step UI Components
Three built-in components for step progress and navigation. They render automatically inside <Form /> when your form has multiple steps, but you can also use them standalone.
StepsNavigation
Tab-style navigation with clickable step buttons. Shows numbered circles for each step with checkmark/error icons for completed/invalid steps.
import { StepsNavigation } from '@saastro/forms';
<StepsNavigation
steps={steps}
currentStepId={currentStepId}
stepHistory={stepHistory}
fields={fields}
onStepClick={(stepId) => goToStep(stepId)}
orientation="horizontal" // 'horizontal' (default) | 'vertical'
showLabels={true} // Show step labels (default: true)
showFieldCount={false} // Show field count badges (default: false)
allowNavigation={true} // Allow clicking to navigate (default: true)
/>;
When to use: Most multi-step forms. Gives users a clear overview of all steps and lets them jump back to completed steps.
StepsProgress
Lightweight progress indicator with three visual variants:
import { StepsProgress } from '@saastro/forms';
// Progress bar (default)
<StepsProgress
steps={steps}
currentStepId={currentStepId}
stepHistory={stepHistory}
fields={fields}
variant="bar" // 'bar' (default) | 'dots' | 'steps'
showLabels={false} // Show step labels (default: false)
showPercentage={true} // Show "60%" text (default: true)
/>
// Dots — minimal indicator
<StepsProgress variant="dots" {...stepProps} />
// Steps — numbered step indicators
<StepsProgress variant="steps" showLabels={true} {...stepProps} />
When to use: When you want something lighter than full navigation tabs. bar for simple flows, dots for minimal UI, steps for numbered progress.
StepsAccordion
Accordion layout where each step is a collapsible section. Users can expand any visited step to review their answers.
import { StepsAccordion } from '@saastro/forms';
<StepsAccordion
steps={steps}
currentStepId={currentStepId}
stepHistory={stepHistory}
fields={fields}
onStepSelect={(stepId) => goToStep(stepId)}
allowNavigation={true} // Allow clicking to expand/navigate (default: true)
showFieldCount={true} // Show "3 fields" badges (default: true)
showErrors={true} // Show error indicators (default: true)
showProgress={true} // Show completion checkmarks (default: true)
/>;
When to use: Long forms where users need to review previous answers. Works well for applications, surveys, and checkout flows.
Automatic Rendering
You don’t need to use these components directly. The <Form /> component renders StepsNavigation automatically when the config has multiple steps:
// This form will show step navigation tabs automatically
<Form config={multiStepConfig} components={uiComponents} />
Step Metadata
The useFormStepsInfo hook provides rich metadata for building custom step UI:
import { useFormStepsInfo } from '@saastro/forms';
function CustomStepUI({ steps, currentStepId, stepHistory, fields }) {
const info = useFormStepsInfo({ steps, currentStepId, stepHistory, fields });
return (
<div>
<p>
Step {info.steps.findIndex((s) => s.isCurrent) + 1} of {info.totalSteps}
</p>
<p>Progress: {info.progress}%</p>
{info.steps.map((step) => (
<span key={step.id} className={step.status}>
{step.id} ({step.status})
</span>
))}
</div>
);
}
FormStepsInfo Return Value
| Property | Type | Description |
|---|---|---|
steps | StepInfo[] | Array of step metadata |
currentStepId | string | Current step ID |
totalSteps | number | Total number of steps |
completedSteps | number | Number of completed steps |
progress | number | Completion percentage (0-100) |
stepHistory | string[] | Stack of visited step IDs |
canGoNext | boolean | Whether next navigation is possible |
canGoPrev | boolean | Whether back navigation is possible |
getStepInfo(id) | (id: string) => StepInfo | Get info for a specific step |
getStepStatus(id) | (id: string) => StepStatus | Get status for a specific step |
StepInfo
| Property | Type | Description |
|---|---|---|
id | string | Step ID |
fields | string[] | Field names in this step |
fieldCount | number | Number of fields |
status | StepStatus | 'pending', 'current', 'completed', or 'error' |
hasErrors | boolean | Whether any fields have validation errors |
errorCount | number | Number of fields with errors |
isVisited | boolean | Whether step has been visited |
isCurrent | boolean | Whether this is the active step |
isCompleted | boolean | Whether all fields are valid and filled |
canNavigate | boolean | Whether the user can navigate to this step |
Per-Step Validation
Fields are validated before advancing to the next step. Only the fields in the current step are checked — fields in other steps are not validated until their step is active.
If validation fails, the user stays on the current step and error messages appear on the invalid fields.
Complete Example
A multi-step onboarding wizard with conditional routing:
import { FormBuilder, Form } from '@saastro/forms';
const config = FormBuilder.create('onboarding')
.layout('manual')
.columns(12)
// Step 1: Account Type
.addField('accountType', (f) =>
f
.type('button-radio')
.label('Account Type')
.required()
.options([
{ value: 'personal', label: 'Personal' },
{ value: 'business', label: 'Business' },
])
.columns({ default: 12 }),
)
// Step 2a: Personal Info
.addField('fullName', (f) =>
f.type('text').label('Full Name').required().columns({ default: 12 }),
)
.addField('birthDate', (f) =>
f.type('date').label('Date of Birth').required().columns({ default: 12, md: 6 }),
)
// Step 2b: Business Info
.addField('companyName', (f) =>
f.type('text').label('Company Name').required().columns({ default: 12, md: 6 }),
)
.addField('employees', (f) =>
f
.type('select')
.label('Company Size')
.required()
.options([
{ value: '1-10', label: '1-10' },
{ value: '11-50', label: '11-50' },
{ value: '51-200', label: '51-200' },
{ value: '200+', label: '200+' },
])
.columns({ default: 12, md: 6 }),
)
// Step 3: Confirmation
.addField('newsletter', (f) => f.type('switch').label('Subscribe to newsletter').optional())
.addField('terms', (f) =>
f
.type('checkbox')
.label('I accept the terms and conditions')
.mustBeTrue('You must accept the terms'),
)
// Define steps with conditional routing
.addStep('account-type', ['accountType'], {
defaultNext: 'personal-info',
next: [
{
conditions: [{ field: 'accountType', operator: 'equals', value: 'business' }],
operator: 'AND',
target: 'business-info',
},
],
})
.addStep('personal-info', ['fullName', 'birthDate'], { defaultNext: 'confirm' })
.addStep('business-info', ['companyName', 'employees'], { defaultNext: 'confirm' })
.addStep('confirm', ['newsletter', 'terms'])
.initialStep('account-type')
.buttons({
align: 'between',
submit: { type: 'submit', label: 'Create Account' },
next: { type: 'next', label: 'Continue' },
back: { type: 'back', label: 'Back', variant: 'outline' },
})
.onSuccess(async (values) => {
await fetch('/api/onboarding', {
method: 'POST',
body: JSON.stringify(values),
});
})
.build();
// The flow:
// account-type → personal-info → confirm (if "personal")
// account-type → business-info → confirm (if "business")