Hotwire-powered multi-step forms

Jordan Lee Jan 2026
3 tabs
class WizardsController < ApplicationController
  before_action :load_draft

  def step_1
    render_step(1)
  end

  def update_step_1
    if update_draft(step_1_params)
      redirect_to wizard_step_2_path
    else
      render_step(1, status: :unprocessable_entity)
    end
  end

  def step_2
    render_step(2)
  end

  def update_step_2
    if update_draft(step_2_params)
      redirect_to wizard_step_3_path
    else
      render_step(2, status: :unprocessable_entity)
    end
  end

  def step_3
    render_step(3)
  end

  def create
    if @draft.update(step_3_params.merge(completed: true))
      session.delete(:wizard_draft_id)
      redirect_to @draft, notice: 'Application submitted successfully!'
    else
      render_step(3, status: :unprocessable_entity)
    end
  end

  private

  def load_draft
    @draft = WizardDraft.find_or_create_by(id: session[:wizard_draft_id], user: current_user)
    session[:wizard_draft_id] = @draft.id
  end

  def update_draft(params)
    @draft.update(params)
  end

  def render_step(step, status: :ok)
    render "wizards/step_#{step}", locals: { draft: @draft }, status: status
  end

  def step_1_params
    params.require(:wizard_draft).permit(:name, :email, :phone)
  end

  def step_2_params
    params.require(:wizard_draft).permit(:address, :city, :state, :zip)
  end

  def step_3_params
    params.require(:wizard_draft).permit(:preferences, :notes)
  end
end
3 files · ruby, erb Explain with highlit

Multi-step forms traditionally require complex JavaScript state management, but Hotwire makes them simple. Each step is a separate controller action that renders a Turbo Frame containing the current step's fields. Navigation between steps updates only the frame, preserving completed data in the session or a draft record. I validate each step server-side before allowing progress to the next. Back/forward buttons work naturally with Turbo's history management. This approach keeps form logic in Rails—validation, defaults, conditional fields—while delivering a SPA-like experience. The form submits normally on the final step, and server-side validation is the single source of truth throughout.