Building a sign-up or onboarding wizard shouldn’t be difficult if you have the right composition.

A wireframe of a sign-up wizard

The wizard is a tried and true design interaction pattern. It’s great for on-boarding new users or inputting information tied to some type of decision tree.

Under the hood, a wizard nothing more than a state machine. A state machine is a simple object that keeps track of what state it’s in and the states it can transition to.

EXAMPLE: A Setup Wizard Flow from Kaspersky. Each page on a wizard is a different state. The Next and Previous buttons on the wizard are used to transition between change states.

Wanna see some code? Here’s a github repo with these concepts applied and a live demo.

Setting up a Wizard

In React, a basic wizard looks like this:

It keep track the current step in state and passes that down to the steps of the wizard. Our “state machine” is just a number. A set of Prev/Next buttons increment and decrement the currentStep . (Later on, I’ll explain why that’s not such a good idea)

Each of the children components receives the current step of the wizard as props and can determine whether or not to display based on the value in that prop.

If the currentStep is not correct, we bail early with a guard clause.

This is a basic pattern for a composition based wizard. It’s easy to modify — you can use CSS to show/hide steps instead of a guard clause, or use a renderStep function to do a lookup on a decision tree to return the correct wizard step. The only limit is your imagination!

Let’s build on this foundation to deal with user input in each step.

Handing User Input in Each Step

In many cases, it’s a good idea to make the steps responsible for their own data, since it keeps things simple. If you don’t, you could end up with a very big wizard step wrapper component that handles the sanitizing, validation, and transformation of user input from all the steps in one place, and that will be hard to maintain.

Since each step is responsible for it’s own data, it should have some type of validation and handle any transformation of user input into an appropriate data structure. This also means that each form will have it’s own button that will move the currentStep forward or backwards.

The basic wizard component outlined earlier will have to loose those Prev/Next buttons since the _next and _prev functions will be passed in as props.

And a step will look like this:

Note how this example is also missing a Prev button and _prev . I did this to save time writing the examples. Many wizards, such as a sign-up wizard, don’t need a button that takes the user to the previous step. If this example did have that button, the _prev function would mirror the _next function.

Linkable Steps with location.hash

In some cases, it’s good to have the wizard steps be linkable (i.e. www.example.com/sign_up#step2). It’s pretty easy to handle this hash based navigation using vanilla JS:

State Machines are Cool

Tying this all together is the state machine that manages the current step.

If you’re working on a wizard that only has one possible set of screens — meaning that the choices the user makes don’t affect what screens are shown — the state machine outlined in the examples should work fine.

The path that a user follows to complete a wizard is made up of a series of wizard steps. When the required wizard steps change based on information the user has input (for example, signing up for a business account vs. a personal account), that’s when it’s necessary to use a more robust state machine to manage the user’s path. Having a place where the available steps are checked also makes it easier to do validations on past data and re-surface required or skipped steps.

A contrived example of a complicated wizard that can be easily handled with a robust state machine

State machines are a huge topic for another post. All I can say for now is that it’s very easy to use one in concert with the components and guidelines I’ve outlined above. The wizard is really the display mechanism for the state machine.

Don’t forget to check out the live demo and source code to see these concepts in action.