You built the product. Someone signed up. And then they stared at a blank dashboard and left.
This happens to almost every SaaS at launch. The problem is not your product -- it is that a new user arrives with no context, no first task, and no reason to stay. A multi-step onboarding wizard fixes this by collecting what you need to know, surfacing the right starting point, and making the user feel guided rather than dropped.
Asking a new user to fill out eight fields before they see any value feels like homework. A wizard breaks the same questions into steps of two or three, each with a clear heading and a progress bar. The perceived effort drops even though the total input is identical.
More importantly, a wizard creates a natural place to branch. If a user says they are a solo founder, you skip the team size question. If they say they run an agency, you pre-populate the workspace name. You learn something useful, the user spends less time, and you can route them to the right starting page at the end.
That routing decision -- where do I send this user after they finish? -- is what makes onboarding worth building at all.
A multi-step wizard in Next.js has three parts.
A controlled form state machine. You hold the current step index and all collected values in a single state object. React Hook Form handles field validation per step with Zod schemas, but the values accumulate across steps instead of being submitted until the final step.
A persistence layer for partial progress. If the user closes the browser on step 2, they should pick up where they left off. You store the current step index and accumulated answers in a jsonb column on the user row in Drizzle ORM. The server component reads it on load and passes it as the initial state to the wizard client component.
A completion handler. When the user finishes the last step, you write their data to the relevant tables -- profile, workspace, preferences -- mark onboarding as done, and redirect. The onboarding checklist pattern pairs well here: the wizard collects what you need upfront, and the checklist tracks what the user still needs to do.
Here is a typical onboarding sequence for a B2B SaaS:
Step 1 -- About you
Full name
Role: solo founder / team member / agency
Step 2 -- Your workspace
Workspace name
What are you building? (short answer)
Step 3 -- Your goal
What do you want to accomplish first? (choose one)
Each step has its own Zod schema. The wizard calls React Hook Form's trigger() to validate only the current step's fields before advancing. The full handleSubmit() only fires on the last step.
// Validate only the current step's fields before moving forward
const valid = await form.trigger(STEP_FIELDS[currentStep])
if (!valid) return
setCurrentStep(prev => prev + 1)
This is the key insight from the form validation guide: validate incrementally, accumulate globally. React Hook Form's trigger() accepts a field name or array of field names -- pass only the fields that belong to the current step.
Two columns on the user row handle persistence:
onboarding_step -- integer for the last completed step index (0 means not started)onboarding_data -- jsonb that holds the accumulated answersOn each step completion you PATCH the user via your settings API route. When the user returns, the server component reads onboarding_step and onboarding_data, then passes them as initial props to the wizard client component, which resumes exactly where they left off.
This matters more than it sounds. People sign up from their phone during a commute and come back on a laptop later. Resumable state is a trust signal that your product is polished.
The real payoff of a wizard is what you do with the answers after step 1.
If role === "solo founder", skip the team size question entirely. If goal === "launch my first product", redirect to the quick-start page instead of the blank dashboard. These branches are if statements in your step configuration -- not complex routing logic.
The step array is data, not hardcoded JSX:
const steps = [
{ id: "about", fields: ["name", "role"] },
{ id: "workspace", fields: ["workspaceName", "buildingWhat"],
show: (values) => true },
{ id: "goal", fields: ["firstGoal"] },
]
// Skip steps that don't apply to this user
const visibleSteps = steps.filter(s => !s.show || s.show(form.getValues()))
This keeps the wizard logic declarative and easy to extend without nesting a tangle of conditional components.
If you start from the Next.js SaaS boilerplate, the infrastructure is already in place:
onboarding_step and onboarding_data columnsYou add the wizard UI and step configuration. The foundation -- schema, repo layer, API route, auth -- is already there.
If your app still shows an empty state on first login, that is the most important conversion problem you have. A three-step wizard -- about you, your workspace, your goal -- is enough to cut first-week churn and route users to a starting point that makes sense for them.
Get the boilerplate and add your onboarding wizard this weekend: boilerplate.iteam-company.com.