DEV Community

Eelco Wiersma
Eelco Wiersma

Posted on

Building a multi-step onboarding flow in 5 minutes

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

Saas UI 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

git clone git@github.com:saas-js/saas-ui-nextjs-typescript.git
Enter fullscreen mode Exit fullscreen mode

After closing let's install all dependencies.

yarn
Enter fullscreen mode Exit fullscreen mode

Existing project

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

yarn add @saas-ui/react @chakra-ui/react @emotion/react@^11 @emotion/styled@^11 framer-motion@^6
Enter fullscreen mode Exit fullscreen mode

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

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

import { NextPage } from 'next'

const OnboardingPage : NextPage = () => {
  return (
    <Box>
      Onboarding
    </Box>
  )
}
export default OnboardingPage
Enter fullscreen mode Exit fullscreen mode

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.

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

Enter fullscreen mode Exit fullscreen mode

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.

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

Enter fullscreen mode Exit fullscreen mode

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.

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
Enter fullscreen mode Exit fullscreen mode

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 component. We'll add a custom Select with the company size options.

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
Enter fullscreen mode Exit fullscreen mode

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

Now we still miss an important part, the Stepper, let's add this now. We can simply wrap the steps with the FormStepper component and add a title to the steps.

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
Enter fullscreen mode Exit fullscreen mode

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.

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
Enter fullscreen mode Exit fullscreen mode

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.

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
Enter fullscreen mode Exit fullscreen mode

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

You can find the fully working example here.

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 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! 🤓

Discussion (0)