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
After closing let's install all dependencies.
yarn
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
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
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
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
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
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
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
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
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
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! 🤓
Top comments (2)
Good job! but even a screenshot or a codeSanbox of what to expect after a long coding session would be a great place to start.
Cheers
Exactly! A UI tutorial with no screenshots.