Building a multi-step onboarding flow in 5 minutes
Tags: react, saas, nextjs, typescript
Published: 2022-06-16 15:24:10

In this post I will show you how easy it to build a multi-step onboarding flow with Saas UI.

> [Saas UI](https://saas-ui.dev) is a component library build on top of Chakra UI and contains a lot of components currently missing in Chakra UI as well as higher order components that help boost your productivity.

You typically find onboarding flows during or after signing up for a new product and can consist of a couple of steps to help you setup your new account.

## The tech

The stack that we will be using:

- Saas UI
- Chakra UI
- Next.js
- React Hook Form (used internally by Saas UI)

## The requirements

For this example we will be building a form with 3 steps. It should have a stepper to indicate in which step the user currently is, validation and it should work well on small screens.

### Information
Here we will ask for some personal information as well as business information.

First name, last name, company, if a company name is entered, ask for the company size. 

### Create workspace
Workspace name and url

### Invite team members
Be able to enter multiple email addresses.


## 1. Installation

I've prepared a starter repository to help you get started quickly. [Get it from Github](https://github.com/saas-js/saas-ui-nextjs-typescript)

```bash
git clone git@github.com:saas-js/saas-ui-nextjs-typescript.git
```

After closing let's install all dependencies.

```bash
yarn
```

### Existing project

If you want to continue in an existing Next.js project you can run this to install all required dependencies.

```bash
yarn add @saas-ui/react @chakra-ui/react @emotion/react@^11 @emotion/styled@^11 framer-motion@^6
```

## 2. Create the onboarding page

So first thing we need to do is create a page for the onboarding flow.

Create a new page; `pages/onboarding.tsx`

```ts
import { Box } from '@chakra-ui/react'

import { NextPage } from 'next'

const OnboardingPage : NextPage = () => {
  return (
    <Box>
      Onboarding
    </Box>
  )
}
export default OnboardingPage
```

Now open your browser and visit:

`https://localhost:3000/onboarding`

If everything went right, you should see `Onboarding` now. 👊

Now let's make the page look a bit better by adding a title and let's create the first form step.

```tsx
import { Container, Heading, VStack } from '@chakra-ui/react'
import {
  Card,
  CardBody,
  Field,
  FormLayout,
  FormStep,
  NextButton,
  StepForm,
} from '@saas-ui/react'

import { NextPage } from 'next'

const OnboardingPage: NextPage = () => {
  const onSubmit = async (data) => {
    console.log(data)
  }

  return (
    <Container maxW="container.xl" pt="20">
      <VStack spacing="8">
        <Heading size="lg" textAlign="center">
          Welcome to ACME Corp
        </Heading>

        <StepForm onSubmit={onSubmit} width="420px">
          <FormStep name="information">
            <Card>
              <CardBody>
                <FormLayout>
                  <Field name="firstName" label="First name" />
                  <Field name="lastName" label="Last name" />
                  <NextButton />
                </FormLayout>
              </CardBody>
            </Card>
          </FormStep>
        </StepForm>
      </VStack>
    </Container>
  )
}
export default OnboardingPage

```

Looks good already doesn't it? We now have a fully working multi step form. Try it out by entering some information and pressing complete. You should see the values in the console, ready to post to your backend. 🚀

StepForm (and Form) use `React Hook Form` internally to manage the form state, it allows you to build forms super fast without a lot of boilerplate.

Now let's add the rest of the fields and the other 2 steps to make it truly multi step.

```tsx
import { ButtonGroup, Container, Heading, VStack } from '@chakra-ui/react'
import {
  Card,
  CardBody,
  Field,
  FormLayout,
  FormStep,
  NextButton,
  PrevButton,
  StepForm,
} from '@saas-ui/react'

import { NextPage } from 'next'

const OnboardingPage: NextPage = () => {
  const onSubmit = async (data) => {
    console.log(data)
  }

  return (
    <Container maxW="container.xl" pt="20">
      <VStack spacing="8">
        <Heading size="lg" textAlign="center">
          Welcome to ACME Corp
        </Heading>

        <StepForm onSubmit={onSubmit} width="420px">
          <FormStep name="information">
            <Card>
              <CardBody>
                <FormLayout>
                  <FormLayout columns={2}>
                    <Field name="firstName" label="First name" />
                    <Field name="lastName" label="Last name" />
                  </FormLayout>
                  <Field name="company" label="Company name" />
                  <NextButton />
                </FormLayout>
              </CardBody>
            </Card>
          </FormStep>
          <FormStep name="workspace">
            <Card>
              <CardBody>
                <FormLayout>
                  <Field name="name" label="Workspace name" />
                  <Field name="url" label="Workspace url" />
                  <ButtonGroup>
                    <NextButton />
                    <PrevButton />
                  </ButtonGroup>
                </FormLayout>
              </CardBody>
            </Card>
          </FormStep>
          <FormStep name="invite">
            <Card>
              <CardBody>
                <FormLayout>
                  <Field
                    name="email"
                    label="Invite your teammembers"
                    help="Add multiple addresses by separating them with a comma (,)"
                    type="textarea"
                  />
                  <ButtonGroup>
                    <NextButton />
                    <PrevButton />
                  </ButtonGroup>
                </FormLayout>
              </CardBody>
            </Card>
          </FormStep>
        </StepForm>
      </VStack>
    </Container>
  )
}
export default OnboardingPage

```

Awesome we can now move back and forth through our steps. When you complete the last step you should see all your field values being logged in the console. Sweet!

The next step is to add some validation, because we can complete all steps now without entering any information.

```tsx
import { ButtonGroup, Container, Heading, VStack } from '@chakra-ui/react'
import {
  Card,
  CardBody,
  Field,
  FormLayout,
  FormStep,
  NextButton,
  PrevButton,
  StepForm,
} from '@saas-ui/react'

import { NextPage } from 'next'

const OnboardingPage: NextPage = () => {
  const onSubmit = async (data) => {
    console.log(data)
  }

  return (
    <Container maxW="container.xl" pt="20">
      <VStack spacing="8">
        <Heading size="lg" textAlign="center">
          Welcome to ACME Corp
        </Heading>

        <StepForm onSubmit={onSubmit} width="420px" noValidate>
          <FormStep name="information">
            <Card>
              <CardBody>
                <FormLayout>
                  <FormLayout columns={2}>
                    <Field
                      name="firstName"
                      label="First name"
                      isRequired
                      rules={{ required: 'Please enter your first name.' }}
                    />
                    <Field
                      name="lastName"
                      label="Last name"
                      isRequired
                      rules={{ required: 'Please enter your last name.' }}
                    />
                  </FormLayout>
                  <Field name="company" label="Company name" />
                  <NextButton />
                </FormLayout>
              </CardBody>
            </Card>
          </FormStep>
          <FormStep name="workspace">
            <Card>
              <CardBody>
                <FormLayout>
                  <Field
                    name="name"
                    label="Workspace name"
                    isRequired
                    rules={{ required: 'Please enter a name ' }}
                  />
                  <Field
                    name="url"
                    label="Workspace url"
                    help="We will create one for you if you leave this empty."
                  />
                  <ButtonGroup>
                    <NextButton />
                    <PrevButton />
                  </ButtonGroup>
                </FormLayout>
              </CardBody>
            </Card>
          </FormStep>
          <FormStep name="invite">
            <Card>
              <CardBody>
                <FormLayout>
                  <Field
                    name="emails"
                    label="Invite your teammembers"
                    help="Add multiple addresses by separating them with a comma (,)"
                    type="textarea"
                  />
                  <ButtonGroup>
                    <NextButton />
                    <PrevButton />
                  </ButtonGroup>
                </FormLayout>
              </CardBody>
            </Card>
          </FormStep>
        </StepForm>
      </VStack>
    </Container>
  )
}
export default OnboardingPage
```

That was easy! `rules` accepts all React Hook Form rules and the form also accepts schema resolvers, but more on this in another post.

> Note that we added `noValidate` to the form, to disable the native browser validation messages.

Ok, so we want to ask a little bit more information when a company signed up. Let's add this now, we can simply do this with the [DisplayIf](https://www.saas-ui.dev/docs/forms/form#conditionally-show-fields) component. We'll add a custom [Select](https://www.saas-ui.dev/docs/forms/select) with the company size options.

```tsx
import { ButtonGroup, Container, Heading, VStack } from '@chakra-ui/react'
import {
  Card,
  CardBody,
  DisplayIf,
  Field,
  FormLayout,
  FormStep,
  NextButton,
  PrevButton,
  StepForm,
} from '@saas-ui/react'

import { NextPage } from 'next'

const OnboardingPage: NextPage = () => {
  const onSubmit = async (data) => {
    console.log(data)
  }

  return (
    <Container maxW="container.xl" pt="20">
      <VStack spacing="8">
        <Heading size="lg" textAlign="center">
          Welcome to ACME Corp
        </Heading>

        <StepForm onSubmit={onSubmit} width="420px" noValidate>
          <FormStep name="information">
            <Card>
              <CardBody>
                <FormLayout>
                  <FormLayout columns={2}>
                    <Field
                      name="firstName"
                      label="First name"
                      isRequired
                      rules={{ required: 'Please enter your first name.' }}
                    />
                    <Field
                      name="lastName"
                      label="Last name"
                      isRequired
                      rules={{ required: 'Please enter your last name.' }}
                    />
                  </FormLayout>
                  <Field name="company" label="Company name" />
                  <DisplayIf name="company">
                    <Field
                      name="companySize"
                      label="Company size"
                      placeholder="Select your company size"
                      type="select"
                      options={[
                        {
                          value: '1',
                          label: '1 to 5',
                        },
                        {
                          value: '5',
                          label: '5 to 20',
                        },
                        {
                          value: '20',
                          label: '20 or more',
                        },
                      ]}
                    />
                  </DisplayIf>
                  <NextButton />
                </FormLayout>
              </CardBody>
            </Card>
          </FormStep>
          <FormStep name="workspace">
            <Card>
              <CardBody>
                <FormLayout>
                  <Field
                    name="name"
                    label="Workspace name"
                    isRequired
                    rules={{ required: 'Please enter a name ' }}
                  />
                  <Field
                    name="url"
                    label="Workspace url"
                    help="We will create one for you if you leave this empty."
                  />
                  <ButtonGroup>
                    <NextButton />
                    <PrevButton />
                  </ButtonGroup>
                </FormLayout>
              </CardBody>
            </Card>
          </FormStep>
          <FormStep name="invite">
            <Card>
              <CardBody>
                <FormLayout>
                  <Field
                    name="emails"
                    label="Invite your teammembers"
                    help="Add multiple addresses by separating them with a comma (,)"
                    type="textarea"
                  />
                  <ButtonGroup>
                    <NextButton />
                    <PrevButton />
                  </ButtonGroup>
                </FormLayout>
              </CardBody>
            </Card>
          </FormStep>
        </StepForm>
      </VStack>
    </Container>
  )
}
export default OnboardingPage
```

Bam! Conditional fields without any difficult logic or if/else statements.

Now we still miss an important part, the [Stepper](https://www.saas-ui.dev/docs/navigation/stepper), let's add this now. We can simply wrap the steps with the [FormStepper](https://www.saas-ui.dev/docs/forms/step-form#with-a-stepper) component and add a title to the steps.

```tsx
import { ButtonGroup, Container, Heading, VStack } from '@chakra-ui/react'
import {
  Card,
  CardBody,
  DisplayIf,
  Field,
  FormLayout,
  FormStep,
  FormStepper,
  NextButton,
  PrevButton,
  StepForm,
} from '@saas-ui/react'

import { NextPage } from 'next'

const OnboardingPage: NextPage = () => {
  const onSubmit = async (data) => {
    console.log(data)
  }

  return (
    <Container maxW="container.xl" pt="20">
      <VStack spacing="8">
        <Heading size="lg" textAlign="center">
          Welcome to ACME Corp
        </Heading>

        <StepForm onSubmit={onSubmit} width="420px" noValidate>
          <FormStepper>
            <FormStep name="information" title="Information">
              <Card>
                <CardBody>
                  <FormLayout>
                    <FormLayout columns={2}>
                      <Field
                        name="firstName"
                        label="First name"
                        isRequired
                        rules={{ required: 'Please enter your first name.' }}
                      />
                      <Field
                        name="lastName"
                        label="Last name"
                        isRequired
                        rules={{ required: 'Please enter your last name.' }}
                      />
                    </FormLayout>
                    <Field name="company" label="Company name" />
                    <DisplayIf name="company">
                      <Field
                        name="companySize"
                        label="Company size"
                        placeholder="Select your company size"
                        type="select"
                        options={[
                          {
                            value: '1',
                            label: '1 to 5',
                          },
                          {
                            value: '5',
                            label: '5 to 20',
                          },
                          {
                            value: '20',
                            label: '20 or more',
                          },
                        ]}
                      />
                    </DisplayIf>
                    <NextButton />
                  </FormLayout>
                </CardBody>
              </Card>
            </FormStep>
            <FormStep name="workspace" title="Workspace">
              <Card>
                <CardBody>
                  <FormLayout>
                    <Field
                      name="name"
                      label="Workspace name"
                      isRequired
                      rules={{ required: 'Please enter a name ' }}
                    />
                    <Field
                      name="url"
                      label="Workspace url"
                      help="We will create one for you if you leave this empty."
                    />
                    <ButtonGroup>
                      <NextButton />
                      <PrevButton />
                    </ButtonGroup>
                  </FormLayout>
                </CardBody>
              </Card>
            </FormStep>
            <FormStep name="invite" title="Invite team">
              <Card>
                <CardBody>
                  <FormLayout>
                    <Field
                      name="emails"
                      label="Invite your teammembers"
                      help="Add multiple addresses by separating them with a comma (,)"
                      type="textarea"
                    />
                    <ButtonGroup>
                      <NextButton />
                      <PrevButton />
                    </ButtonGroup>
                  </FormLayout>
                </CardBody>
              </Card>
            </FormStep>
          </FormStepper>
        </StepForm>
      </VStack>
    </Container>
  )
}
export default OnboardingPage
```

The last requirement we have is to make the form work well on small screens. Luckily the Stepper supports a vertical orientation, we can use this in combination with `useBreakpointValue`. We'll also make sure the first and last name is rendered under each other on mobile screens.

```tsx
import {
  ButtonGroup,
  Container,
  Heading,
  useBreakpointValue,
  VStack,
} from '@chakra-ui/react'
import {
  Card,
  CardBody,
  DisplayIf,
  Field,
  FormLayout,
  FormStep,
  FormStepper,
  NextButton,
  PrevButton,
  StepForm,
} from '@saas-ui/react'

import { NextPage } from 'next'

const OnboardingPage: NextPage = () => {
  const onSubmit = async (data) => {
    console.log(data)
  }

  return (
    <Container maxW="container.xl" pt="20">
      <VStack spacing="8">
        <Heading size="lg" textAlign="center">
          Welcome to ACME Corp
        </Heading>

        <StepForm onSubmit={onSubmit} width="420px" noValidate>
          <FormStepper
            orientation={useBreakpointValue({
              base: 'vertical',
              md: 'horizontal',
            })}
          >
            <FormStep name="information" title="Information">
              <Card>
                <CardBody>
                  <FormLayout>
                    <FormLayout columns={{ base: 1, md: 2 }}>
                      <Field
                        name="firstName"
                        label="First name"
                        isRequired
                        rules={{ required: 'Please enter your first name.' }}
                      />
                      <Field
                        name="lastName"
                        label="Last name"
                        isRequired
                        rules={{ required: 'Please enter your last name.' }}
                      />
                    </FormLayout>
                    <Field name="company" label="Company name" />
                    <DisplayIf name="company">
                      <Field
                        name="companySize"
                        label="Company size"
                        placeholder="Select your company size"
                        type="select"
                        options={[
                          {
                            value: '1',
                            label: '1 to 5',
                          },
                          {
                            value: '5',
                            label: '5 to 20',
                          },
                          {
                            value: '20',
                            label: '20 or more',
                          },
                        ]}
                      />
                    </DisplayIf>
                    <NextButton />
                  </FormLayout>
                </CardBody>
              </Card>
            </FormStep>
            <FormStep name="workspace" title="Workspace">
              <Card>
                <CardBody>
                  <FormLayout>
                    <Field
                      name="name"
                      label="Workspace name"
                      isRequired
                      rules={{ required: 'Please enter a name ' }}
                    />
                    <Field
                      name="url"
                      label="Workspace url"
                      help="We will create one for you if you leave this empty."
                    />
                    <ButtonGroup>
                      <NextButton />
                      <PrevButton />
                    </ButtonGroup>
                  </FormLayout>
                </CardBody>
              </Card>
            </FormStep>
            <FormStep name="invite" title="Invite team">
              <Card>
                <CardBody>
                  <FormLayout>
                    <Field
                      name="emails"
                      label="Invite your teammembers"
                      help="Add multiple addresses by separating them with a comma (,)"
                      type="textarea"
                    />
                    <ButtonGroup>
                      <NextButton />
                      <PrevButton />
                    </ButtonGroup>
                  </FormLayout>
                </CardBody>
              </Card>
            </FormStep>
          </FormStepper>
        </StepForm>
      </VStack>
    </Container>
  )
}
export default OnboardingPage
```

Without a blink! 😎

Now we're almost ready, you probably have noticed the type warning in the onSubmit handler. Let's solve this by making the form typesafe.

Your form should something like this now.

```tsx
import {
  ButtonGroup,
  Container,
  Heading,
  useBreakpointValue,
  VStack,
} from '@chakra-ui/react'
import {
  Card,
  CardBody,
  DisplayIf,
  Field,
  FormLayout,
  FormStep,
  FormStepper,
  NextButton,
  PrevButton,
  StepForm,
  SubmitHandler,
} from '@saas-ui/react'

import { NextPage } from 'next'

interface InformationInputs {
  firstName: string
  lastName: string
  company?: string
  companySize: string
}

interface WorkspaceInputs {
  name: string
  url?: string
}

interface InviteInputs {
  email?: string
}

type FormInputs = InformationInputs & WorkspaceInputs & InviteInputs

const OnboardingPage: NextPage = () => {
  const onSubmit: SubmitHandler<FormInputs> = async (data) => {
    console.log(data)
  }

  return (
    <Container maxW="container.xl" pt="20">
      <VStack spacing="8">
        <Heading size="lg" textAlign="center">
          Welcome to ACME Corp
        </Heading>

        <StepForm<FormInputs> onSubmit={onSubmit} width="420px" noValidate>
          <FormStepper
            orientation={useBreakpointValue({
              base: 'vertical',
              md: 'horizontal',
            })}
          >
            <FormStep name="information" title="Information">
              <Card>
                <CardBody>
                  <FormLayout>
                    <FormLayout columns={{ base: 1, md: 2 }}>
                      <Field
                        name="firstName"
                        label="First name"
                        isRequired
                        rules={{ required: 'Please enter your first name.' }}
                      />
                      <Field
                        name="lastName"
                        label="Last name"
                        isRequired
                        rules={{ required: 'Please enter your last name.' }}
                      />
                    </FormLayout>
                    <Field name="company" label="Company name" />
                    <DisplayIf name="company">
                      <Field
                        name="companySize"
                        label="Company size"
                        placeholder="Select your company size"
                        type="select"
                        options={[
                          {
                            value: '1',
                            label: '1 to 5',
                          },
                          {
                            value: '5',
                            label: '5 to 20',
                          },
                          {
                            value: '20',
                            label: '20 or more',
                          },
                        ]}
                      />
                    </DisplayIf>
                    <NextButton />
                  </FormLayout>
                </CardBody>
              </Card>
            </FormStep>
            <FormStep name="workspace" title="Workspace">
              <Card>
                <CardBody>
                  <FormLayout>
                    <Field
                      name="name"
                      label="Workspace name"
                      isRequired
                      rules={{ required: 'Please enter a name ' }}
                    />
                    <Field
                      name="url"
                      label="Workspace url"
                      help="We will create one for you if you leave this empty."
                    />
                    <ButtonGroup>
                      <NextButton />
                      <PrevButton />
                    </ButtonGroup>
                  </FormLayout>
                </CardBody>
              </Card>
            </FormStep>
            <FormStep name="invite" title="Invite team">
              <Card>
                <CardBody>
                  <FormLayout>
                    <Field
                      name="emails"
                      label="Invite your teammembers"
                      help="Add multiple addresses by separating them with a comma (,)"
                      type="textarea"
                    />
                    <ButtonGroup>
                      <NextButton />
                      <PrevButton />
                    </ButtonGroup>
                  </FormLayout>
                </CardBody>
              </Card>
            </FormStep>
          </FormStepper>
        </StepForm>
      </VStack>
    </Container>
  )
}
export default OnboardingPage
```

Great work! We got a fully working multi step form ready in just a few minutes. 

You can find the fully working [example here](https://github.com/saas-js/saas-ui-nextjs-typescript/tree/onboarding).

Now there is a lot more you can add, like hooking up the steps to the router. Adding schema validation and more advanced fields like the [ArrayField](https://www.saas-ui.dev/docs/forms/array-field) to add individual email instead of a comma separated list.

Let me know what you think in the comments and what you would like to see more.

Happy coding! 🤓