Skip to content

Multi-Step Form Workflows

When to Use

You need wizard-style forms with sequential steps, where users navigate forward/backward without page reloads.

Steps

  1. Initialize step tracking
public function buildForm(array $form, FormStateInterface $form_state) {
  $step = $form_state->get('step') ?: 1;
  $form_state->set('step', $step);
}
  1. Build step-specific form elements
switch ($step) {
  case 1:
    $form['step1'] = $this->buildStep1($form_state);
    break;
  case 2:
    $form['step2'] = $this->buildStep2($form_state);
    break;
}
  1. Add navigation buttons with AJAX
$form['actions']['next'] = [
  '#type' => 'submit',
  '#value' => t('Next'),
  '#submit' => ['::nextStep'],
  '#ajax' => [
    'callback' => '::stepCallback',
    'wrapper' => 'form-wrapper',
  ],
];
  1. Implement step transitions
public function nextStep(array &$form, FormStateInterface $form_state) {
  $step = $form_state->get('step');
  $form_state->set('step', $step + 1);
  $form_state->setRebuild();  // CRITICAL: rebuilds form
}
  1. Return entire form from callback
public function stepCallback(array &$form, FormStateInterface $form_state) {
  return $form;  // Return whole form to update all content
}

The form itself must carry the wrapper the navigation buttons target:

$form['#prefix'] = '<div id="form-wrapper">';
$form['#suffix'] = '</div>';

Decision Points

At this step... If... Then...
Navigation First step Hide "Previous" button
Navigation Last step Show "Submit" instead of "Next"
Data persistence Moving between steps Store values in $form_state->set('data', $values)
Validation Step requires validation Use separate submit handler with validation
Progress indication Multiple steps Add progress bar showing current step

Common Mistakes

  • Forgetting $form_state->setRebuild() → Form submits instead of rebuilding, workflow breaks
  • Not wrapping entire form → Update misses navigation buttons; wrap form with #prefix/#suffix containing wrapper ID
  • Validating on navigation buttons → Add #limit_validation_errors => [] to Previous/Next buttons
  • Losing form data between steps → Store in $form_state, not private properties (form rebuilds from scratch)
  • Not conditionally showing buttons → Previous button on step 1, Next button on final step creates poor UX

See Also