A good onboarding flow helps users get value from your product faster. Here is how to build one with step tracking and validation.
Step 1: Define Onboarding Steps
// lib/onboarding.ts
export const ONBOARDING_STEPS = [
{ id: "welcome", title: "Welcome", description: "Tell us about yourself" },
{ id: "company", title: "Company", description: "Your company details" },
{ id: "preferences", title: "Preferences", description: "Customize your experience" },
{ id: "complete", title: "Complete", description: "You're all set!" },
] as const;
export type StepId = (typeof ONBOARDING_STEPS)[number]["id"];
Step 2: Onboarding Layout
// app/(onboarding)/layout.tsx
export default function OnboardingLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="flex min-h-screen items-center justify-center bg-gray-50 dark:bg-gray-950">
<div className="w-full max-w-xl">{children}</div>
</div>
);
}
Step 3: Onboarding Wrapper Component
"use client";
import { useState } from "react";
import { ONBOARDING_STEPS, type StepId } from "@/lib/onboarding";
interface OnboardingData {
name: string;
role: string;
companyName: string;
companySize: string;
industry: string;
goals: string[];
}
export function OnboardingWizard() {
const [currentStep, setCurrentStep] = useState(0);
const [data, setData] = useState<Partial<OnboardingData>>({});
const [saving, setSaving] = useState(false);
function updateData(updates: Partial<OnboardingData>) {
setData((prev) => ({ ...prev, ...updates }));
}
function nextStep() {
if (currentStep < ONBOARDING_STEPS.length - 1) {
setCurrentStep((prev) => prev + 1);
}
}
function prevStep() {
if (currentStep > 0) {
setCurrentStep((prev) => prev - 1);
}
}
async function completeOnboarding() {
setSaving(true);
try {
await fetch("/api/onboarding", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
nextStep(); // Go to complete step
} finally {
setSaving(false);
}
}
return (
<div className="rounded-2xl border bg-white p-8 shadow-sm dark:border-gray-800 dark:bg-gray-900">
{/* Progress Bar */}
<div className="mb-8">
<div className="mb-2 flex justify-between text-xs text-gray-500">
<span>Step {currentStep + 1} of {ONBOARDING_STEPS.length}</span>
<span>{ONBOARDING_STEPS[currentStep].title}</span>
</div>
<div className="h-1.5 overflow-hidden rounded-full bg-gray-200 dark:bg-gray-700">
<div
className="h-full rounded-full bg-blue-600 transition-all duration-300"
style={{
width: `${((currentStep + 1) / ONBOARDING_STEPS.length) * 100}%`,
}}
/>
</div>
</div>
{/* Step Content */}
{currentStep === 0 && (
<WelcomeStep data={data} onChange={updateData} />
)}
{currentStep === 1 && (
<CompanyStep data={data} onChange={updateData} />
)}
{currentStep === 2 && (
<PreferencesStep data={data} onChange={updateData} />
)}
{currentStep === 3 && <CompleteStep />}
{/* Navigation */}
{currentStep < ONBOARDING_STEPS.length - 1 && (
<div className="mt-8 flex justify-between">
<button
onClick={prevStep}
disabled={currentStep === 0}
className="rounded-lg border px-4 py-2 text-sm disabled:opacity-30"
>
Back
</button>
{currentStep === ONBOARDING_STEPS.length - 2 ? (
<button
onClick={completeOnboarding}
disabled={saving}
className="rounded-lg bg-blue-600 px-6 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-50"
>
{saving ? "Saving..." : "Complete Setup"}
</button>
) : (
<button
onClick={nextStep}
className="rounded-lg bg-blue-600 px-6 py-2 text-sm font-medium text-white hover:bg-blue-700"
>
Continue
</button>
)}
</div>
)}
</div>
);
}
Step 4: Welcome Step
function WelcomeStep({
data,
onChange,
}: {
data: Partial<OnboardingData>;
onChange: (d: Partial<OnboardingData>) => void;
}) {
return (
<div className="space-y-6">
<div>
<h2 className="text-2xl font-bold">Welcome!</h2>
<p className="mt-1 text-sm text-gray-500">
Let us personalize your experience.
</p>
</div>
<div className="space-y-4">
<div>
<label className="mb-1 block text-sm font-medium">Your Name</label>
<input
value={data.name || ""}
onChange={(e) => onChange({ name: e.target.value })}
placeholder="Jane Smith"
className="w-full rounded-lg border px-3 py-2 text-sm dark:border-gray-600 dark:bg-gray-800"
/>
</div>
<div>
<label className="mb-1 block text-sm font-medium">Your Role</label>
<select
value={data.role || ""}
onChange={(e) => onChange({ role: e.target.value })}
className="w-full rounded-lg border px-3 py-2 text-sm dark:border-gray-600 dark:bg-gray-800"
>
<option value="">Select a role</option>
<option value="founder">Founder / CEO</option>
<option value="marketing">Marketing</option>
<option value="developer">Developer</option>
<option value="designer">Designer</option>
<option value="other">Other</option>
</select>
</div>
</div>
</div>
);
}
Step 5: Company Step
function CompanyStep({
data,
onChange,
}: {
data: Partial<OnboardingData>;
onChange: (d: Partial<OnboardingData>) => void;
}) {
return (
<div className="space-y-6">
<div>
<h2 className="text-2xl font-bold">About Your Company</h2>
<p className="mt-1 text-sm text-gray-500">
This helps us tailor recommendations.
</p>
</div>
<div className="space-y-4">
<div>
<label className="mb-1 block text-sm font-medium">Company Name</label>
<input
value={data.companyName || ""}
onChange={(e) => onChange({ companyName: e.target.value })}
className="w-full rounded-lg border px-3 py-2 text-sm dark:border-gray-600 dark:bg-gray-800"
/>
</div>
<div>
<label className="mb-1 block text-sm font-medium">Company Size</label>
<div className="grid grid-cols-3 gap-2">
{["1-5", "6-25", "26-100", "101-500", "500+"].map((size) => (
<button
key={size}
onClick={() => onChange({ companySize: size })}
className={`rounded-lg border px-3 py-2 text-sm transition ${
data.companySize === size
? "border-blue-500 bg-blue-50 text-blue-600 dark:bg-blue-900/20"
: "hover:bg-gray-50 dark:hover:bg-gray-800"
}`}
>
{size}
</button>
))}
</div>
</div>
<div>
<label className="mb-1 block text-sm font-medium">Industry</label>
<select
value={data.industry || ""}
onChange={(e) => onChange({ industry: e.target.value })}
className="w-full rounded-lg border px-3 py-2 text-sm dark:border-gray-600 dark:bg-gray-800"
>
<option value="">Select an industry</option>
<option value="saas">SaaS</option>
<option value="ecommerce">E-commerce</option>
<option value="healthcare">Healthcare</option>
<option value="finance">Finance</option>
<option value="education">Education</option>
<option value="other">Other</option>
</select>
</div>
</div>
</div>
);
}
Step 6: Preferences Step
function PreferencesStep({
data,
onChange,
}: {
data: Partial<OnboardingData>;
onChange: (d: Partial<OnboardingData>) => void;
}) {
const goals = [
"Increase website traffic",
"Generate more leads",
"Improve conversion rates",
"Build a new website",
"Redesign existing site",
"Build a web application",
];
function toggleGoal(goal: string) {
const current = data.goals || [];
const updated = current.includes(goal)
? current.filter((g) => g !== goal)
: [...current, goal];
onChange({ goals: updated });
}
return (
<div className="space-y-6">
<div>
<h2 className="text-2xl font-bold">Your Goals</h2>
<p className="mt-1 text-sm text-gray-500">
Select all that apply.
</p>
</div>
<div className="grid grid-cols-2 gap-2">
{goals.map((goal) => (
<button
key={goal}
onClick={() => toggleGoal(goal)}
className={`rounded-lg border p-3 text-left text-sm transition ${
(data.goals || []).includes(goal)
? "border-blue-500 bg-blue-50 text-blue-600 dark:bg-blue-900/20"
: "hover:bg-gray-50 dark:hover:bg-gray-800"
}`}
>
{goal}
</button>
))}
</div>
</div>
);
}
Step 7: Completion Step
function CompleteStep() {
return (
<div className="text-center">
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-green-100 dark:bg-green-900/20">
<CheckIcon className="h-8 w-8 text-green-600" />
</div>
<h2 className="text-2xl font-bold">You are all set!</h2>
<p className="mt-2 text-sm text-gray-500">
Your account has been configured. Let us get started.
</p>
<a
href="/dashboard"
className="mt-6 inline-block rounded-lg bg-blue-600 px-6 py-2 text-sm font-medium text-white hover:bg-blue-700"
>
Go to Dashboard
</a>
</div>
);
}
Need User Onboarding Built?
We design and build onboarding flows, user experiences, and web applications that drive adoption. Contact us to discuss your project.