How to do multi-step forms in Rails

By Jason Swett, March 23, 2020

Two kinds of multi-step forms

The creation of multi-step forms is a relatively common challenge faced in Rails programming (and probably in web development in general of course). Unlike regular CRUD interfaces, there’s not a prescribed “Rails Way” to do multi-step forms. This, plus the fact that multi-step forms are often inherently complicated, can make them a challenge.

Following is an explanation of I do multi-step forms. First, it’s helpful to understand that there are two types of them. There’s an easy kind and a hard kind.

After I explain what the easy kind and the hard kind of multi-step forms are, I’ll show an illustration of the hard kind.

The easy kind

The easy kind of multi-step form is when each step involves just one Active Record model.

In this scenario you can have a one-to-one mapping between a controller and a step in the form.

For example, let’s say that you have a form with two steps. In the first step you collect information about a user’s vehicle. The corresponding model to this first step is Vehicle. In the second step you collect information about the user’s home. The corresponding model for the second step is Home.

Your multi-step form code could involve two controllers, one called Intake::VehiclesController and the other called Intake::HomesController. (I’m making up the Instake namespace prefix because maybe we’ll decide to call this multi-step form the “intake” form.)

To be clear, the Intake::VehiclesController would exist in addition to the main scaffold-generated VehiclesController, not instead of it. Same of course with the Intake::HomesController. The reason for this is that it’s entirely likely that the “intake” controllers would have different behavior from the main controllers for those resources. For example, Intake::VehiclesController would probably only have the new and create actions, and none of the other ones like delete. Intake::VehiclesController#create action would also probably have a redirect_to that sends the user to the second step, Intake::HomesController#new, once the vehicle information is successfully collected.

To summarize the solution to the easy type of multi step form: For each step of your form, create a controller that corresponds to the Active Record model that’s associated with that step.

Again, this solution only works if your form steps and your Active Record models have a one-to-one relationship. If that’s not the case, you’re probably not dealing with the easy type of multi-step form. You’re probably dealing with the hard kind.

The hard kind

The more difficult kind of multi-step form is when there’s not a tidy one-to-one mapping between models and steps.

Let’s say, for example, that the multi-step form needs to collect user profile/account information. The first step collects first name and last name. The second step collects email and password. All four of these attributes (first name, last name, email and password) exist on the same model, User. How do we validate the first name and last name attributes in the first step when the User model wants to validate a whole User object at a time?

The answer is that we create two new concepts/objects called, perhaps, Intake::UserProfile and Intake::UserAccount. The Intake::UserProfile object knows how to validate first_name and last_name. The Intake::UserAccount knows how to validate email and password. Only after each form step is validated do we attempt to save a User record to the database.

If you found the last paragraph difficult to follow, it’s probably because I was describing a scenario that isn’t very common in Rails applications. I’m talking about creating models that don’t inherit from ActiveRecord::Base but rather that mix in ActiveModel::Model in order to gain some Active-Record-like capabilities.

All this is easier to illustrate with an example than to describe with words, so let’s get into the details of how this might be accomplished.

The tutorial


This tutorial will illustrate a multi-step form where the first step collects “user profile” information (first name and last name) and the second step collects “user account” information (email and password).

Although I’m aware of the Wicked gem, my approach doesn’t use any external libraries. I think gems lend themselves well to certain types of problems/tasks, like tasks where a uniform solution works pretty well for everyone. In my experience multi-step forms are different enough from case to case that a gem to “take out the repetitive work” doesn’t really make sense because most of the time-consuming work is unique to that app, and the rest of the work is easily handled by the benefits that Rails itself already provides.

Here’s the code for my multi-step form, starting with the user profile model.

The user profile model

What will ultimately be created in the database as a result of the user completing the multi-step form is a single User record.

For each of the two forms (again, user profile and user account) we want to be able to validate the form but we don’t necessarily want to persist the data yet. We only want to persist the data after the successful completion of the last step.

One way to achieve this is to create forms that don’t connect to an ActiveRecord::Base object, but instead connect to an ActiveModel::Model object.

If you mix in ActiveModel::Model into a plain old Ruby object, you gain the ability to plug that object into a Rails form just as you would with a regular ActiveRecord object. You also gain the ability to do validations just as you would with a regular ActiveRecord object.

Below I’ve created a class that will be used in the user profile step. I’ve called it UserProfile and put it under an arbitrarily-named Intake namespace.

The user profile controller

This model will connect to a controller I’m calling UserProfilesController. Similar to how I put the UserProfile model class inside a namespace, I’ve put my controller class inside a namespace just so my root-level namespace doesn’t get cluttered up with the complex details of my multi-step form.

I’ve annotated the controller code with comments to explain what’s happening.

The user profile view

Similar to how the user profile controller is very nearly the same as a “regular” controller even though no Active Record class or underlying database table is involved, the user profile form markup is indistinguishable from the code that would be used for an Active-Record-backed class.

The user account model

The user account model follows the exact same principles as the user profile model. Instead of inheriting from ActiveRecord::Base, it mixes in ActiveModel::Model.

The user account controller

The user account controller differs slightly from the user profile controller because the user account controller step is the last step of the multi-step form process. I’ve added annotations to this controller’s code to explain the differences.

The user account view

Like the user profile view, the user account view is indistinguishable from a regular Active Record view.


Lastly, we need to tie everything together with some routing directives.


Below is a recording of me interacting with a multi-step form using all the code shown above.


There are two types of multi-step forms. The easy kind is where each step corresponds to a single Active Record model. In those cases you can use a dedicated controller per step and redirect among the steps. The harder kind is where there’s not a one-to-one mapping. In those cases you can mix in ActiveModel::Model to lend Active-Record-like behaviors to plain old Ruby objects.

In addition to the bare functionality I described above, you can imagine multi-step forms involving more sophisticated requirements, like the ability to go back to previous steps, for example. I didn’t want to clutter my tutorial with those details but I think those behaviors would be manageable enough to add on top of the underlying overall approach I describe.

As a last side note, you don’t have to use session storage to save the data from each form step until the final aggregation at the end. You could store that data any way you want, including using a full-blown Active Record class for each step. There are pros and cons to the various possible ways of doing it. One thing I like about this approach is that I don’t have any “residual” data lying around at the end that I have to clean up. I can imagine other scenarios where other approaches would be more appropriate though, e.g. multi-step forms that the user would want to be able to save and finishing filling out at a later time.

The most important thing in my mind is to keep the multi-step form code easily understandable and to make its code sufficiently modularized (e.g. using namespaces) that it doesn’t clutter up the other concerns in the application.