Saturday, July 12, 2025
Building Dynamic User Journeys: Your Guide to Next.js Onboarding with OnboardJS


Next.js developers love the power and flexibility of the framework, but even with its advantages, building an engaging, multi-step user onboarding experience can quickly become a significant undertaking. Managing step logic, dynamic navigation, persistence across sessions, and analytics can turn your elegant codebase into a complex web of conditional rendering and state management.
Imagine an onboarding library that handles all this complexity for you, letting you focus on crafting beautiful UI components and compelling content, perfectly integrated within your Next.js application.
This is where OnboardJS comes in. It's an open-source, headless onboarding engine designed to abstract away the intricate logic of multi-step flows. This post will guide you through setting up OnboardJS to build a robust Next.js onboarding flow, making your first-user experiences intuitive and efficient.
What is OnboardJS? The Headless Advantage for Next.js
OnboardJS offers a powerful, framework-agnostic onboarding engine (from @onboardjs/core
). This "headless" nature means it focuses solely on the intricate backend logic of your flow:
- Dynamically determining the next/previous step
- Managing the flow's internal state and context
- Handling conditional navigation and step skipping
- Seamlessly integrating with data persistence and analytics layers
- Providing a robust plugin system for extended functionality
For Next.js developers, this headless approach is a perfect fit. The core engine runs independently, and our @onboardjs/react
package provides the necessary hooks and a context provider to integrate it effortlessly into your Next.js Client Components, allowing you to render any step dynamically and respond to user actions.
Before we dive in, we have a full Next.js example on our GitHub so you don't have to start from scratch! All we ask is a ⭐ in return 😉.
Why OnboardJS for Your Next.js App?
- Simplifies Complex Logic: No more tangled if/else trees or manual state machines for your Next.js onboarding flow. OnboardJS handles the entire orchestration.
- SSR/CSR Compatibility: OnboardJS's headless nature means its core logic isn't tied to the DOM, making it ideal for Next.js environments. The React components that consume it live in Client Components, where they belong.
- Extensible by Design: Need to persist user progress to PostgreSQL via Supabase or Neon? Or track onboarding events with PostHog? Our plugin system makes it incredibly easy – and we already offer official plugins for these!
Getting Started: Installation
Let's install the necessary packages for your Next.js project. This tutorial assumes that you already have a Next.js project set up. If you don't have that yet, follow the official Next.js Docs.
1npm install @onboardjs/core @onboardjs/react
Step 1: Define Your Onboarding Steps (Configuration)
The core definition of your Next.js onboarding flow lies in its configuration. This is where you outline each step: its unique ID, its type (e.g., "INFORMATION", "CUSTOM_COMPONENT"), and any specific data (payload) it needs for rendering. This configuration file can be shared between server and client components as needed.
Create a config/onboardingConfig.ts
file (or similar):
1// config/onboardingConfig.ts
2// Optionally, define your custom onboarding context type
3export interface MyAppContext extends OnboardingContext {
4 currentUser?: {
5 id: string;
6 email: string;
7 firstName?: string;
8 };
9 // Add any other global data you need throughout the flow
10}
11
12export const onboardingSteps: OnboardingStep<MyAppContext>[] = [
13 {
14 id: "welcome",
15 payload: {
16 mainText: "Welcome to our product! Let's get you set up.",
17 subText: "This quick tour will guide you through the basics.",
18 },
19 nextStep: "collect-info", // Go to next step by ID
20 },
21 {
22 id: "collect-info",
23 type: "CUSTOM_COMPONENT", // We'll render this with a custom React component
24 payload: {
25 componentKey: "UserProfileForm", // Key to map to your React component
26 formFields: [
27 // Example form field data
28 { id: "name", label: "Your Name", type: "text", dataKey: "userName" },
29 { id: "email", label: "Email", type: "email", dataKey: "userEmail" },
30 ],
31 },
32 // Conditionally skip if user data already exists
33 condition: (context) => !context.currentUser?.firstName,
34 isSkippable: true,
35 skipToStep: "select-plan",
36 },
37 {
38 id: "select-plan",
39 type: "SINGLE_CHOICE",
40 payload: {
41 question: "Which plan are you interested in?",
42 options: [
43 { id: "basic", label: "Basic", value: "basic" },
44 { id: "pro", label: "Pro", value: "pro" },
45 ],
46 dataKey: "chosenPlan", // Data will be stored as flowData.chosenPlan
47 },
48 // nextStep defaults to the next step in array if not specified
49 },
50 {
51 id: "all-done",
52 payload: {
53 mainText: "You're all set!",
54 subText: "Thanks for completing the onboarding.",
55 },
56 },
57];
58
59export const onboardingConfig: OnboardingEngineConfig<MyAppContext> = {
60 steps: onboardingSteps,
61 initialStepId: "welcome", // Start here or from persisted data
62 initialContext: {
63 // Initial global context, can be overwritten by loaded data
64 currentUser: { id: "user_123", email: "test@example.com" },
65 },
66 // You can define load/persist/clearData functions here,
67 // or use the localStoragePersistence option in OnboardingProvider
68};
For more details on defining steps and configurations, refer to the OnboardJS Core Documentation: Configuration.
Step 2: Wrap Your App with OnboardingProvider (Next.js Specifics)
This is a crucial step for Next.js, especially with the App Router. The OnboardingProvider
from @onboardjs/react
must be defined as a Client Component (using "use client";
) because it relies on React Hooks and browser APIs (like localStorage
if used for device persistence). It then makes the OnboardJS engine's state and actions available throughout your Client Component tree.
Create a dedicated Client Component wrapper component (e.g.: OnboardingWrapper.tsx
):
1// app/OnboardingWrapper.tsx
2"use client"; // REQUIRED: This makes the component a Client Component
3
4import { OnboardingProvider } from "@onboardjs/react";
5import { onboardingConfig, type MyAppContext } from "@/config/onboardingConfig"; // Adjust path as needed
6
7export function OnboardingWrapper({ children }: { children: React.ReactNode }) {
8 // Example: Using localStorage for persistence - great for quick demos!
9 const localStoragePersistenceOptions = {
10 key: "onboardjs-demo-state",
11 ttl: 7 * 24 * 60 * 60 * 1000, // 7 days TTL
12 };
13
14 // Optional: Listen for flow completion to clear local storage
15 const handleFlowComplete = async (context: MyAppContext) => {
16 console.log("Onboarding Flow Completed!", context.flowData);
17 // Any final actions like redirecting, showing success message etc.
18 // The provider automatically clears local storage if localStoragePersistence is active.
19 };
20
21 // Optional: Listen for step changes for debugging or custom logic
22 const handleStepChange = (newStep, oldStep, context) => {
23 console.log(
24 `Step changed from ${oldStep?.id || "N/A"} to ${newStep?.id || "N/A"}`,
25 context.flowData,
26 );
27 };
28
29 return (
30 <OnboardingProvider
31 {...onboardingConfig} // Pass your defined steps, initialStepId, initialContext etc.
32 localStoragePersistence={localStoragePersistenceOptions}
33 onFlowComplete={handleFlowComplete}
34 onStepChange={handleStepChange}
35 // You can also pass custom loadData/persistData/clearPersistedData here
36 >
37 {children}
38 </OnboardingProvider>
39 );
40}
41
42// In your root layout.tsx:
43// import { OnboardingWrapper } from "./OnboardingWrapper";
44// export default function RootLayout({ children }) {
45// return (
46// <html>
47// <body>
48// <OnboardingWrapper>{children}</OnboardingWrapper>
49// </body>
50// </html>
51// );
52// }
Step 3: Render Your Current Step with useOnboarding
The useOnboarding
hook gives you access to the current state of the engine (like currentStep
) and actions (next
, previous
, skip
, goToStep
, updateContext
).You'll also need a StepComponentRegistry
to map your step types or step IDs (e.g., "INFORMATION", "CUSTOM_COMPONENT") to actual React components.
First, define your StepComponentRegistry
:
1// config/stepRegistry.tsx
2import React from "react";
3import {
4 useOnboarding,
5 type StepComponentRegistry,
6 type StepComponentProps,
7} from "@onboardjs/react";
8import type { InformationStepPayload } from "@onboardjs/core";
9import type { MyAppContext } from "@/config/onboardingConfig";
10
11// --- Step Components (examples) - Feel free to put these into separate component files ---
12const InformationStep: React.FC<StepComponentProps<InformationStepPayload>> = ({
13 payload,
14}) => {
15 return (
16 <div>
17 <h2 className="text-2xl font-bold mb-4">{payload.mainText}</h2>
18 {payload.subText && <p className="text-gray-600">{payload.subText}</p>}
19 </div>
20 );
21};
22
23const UserProfileFormStep: React.FC<StepComponentProps> = ({
24 payload,
25 coreContext,
26}) => {
27 const { updateContext } = useOnboarding<MyAppContext>();
28 const [userName, setUserName] = React.useState(
29 coreContext.flowData.userName || "",
30 );
31 const [userEmail, setUserEmail] = React.useState(
32 coreContext.flowData.userEmail || "",
33 );
34
35 React.useEffect(() => {
36 // Update the context whenever userName or userEmail changes
37 // Note: There are more sophisticated ways to handle this,
38 // such as debouncing or only updating on form submission.
39 updateContext({ flowData: { userName, userEmail } });
40 }, [userName, userEmail, updateContext]);
41
42 return (
43 <div>
44 <h2 className="text-2xl font-bold mb-4">Tell us about yourself!</h2>
45 <input
46 type="text"
47 placeholder="Your Name"
48 value={userName}
49 onChange={(e) => setUserName(e.target.value)}
50 className="border p-2 rounded mb-2 w-full"
51 />
52 <input
53 type="email"
54 placeholder="Your Email"
55 value={userEmail}
56 onChange={(e) => setUserEmail(e.target.value)}
57 className="border p-2 rounded mb-4 w-full"
58 />
59 {payload.formFields?.map((field: any) => (
60 <div key={field.id}>
61 {/* Render other form fields based on payload.formFields */}
62 </div>
63 ))}
64 </div>
65 );
66};
67
68// Map your step types to React components
69export const stepComponentRegistry: StepComponentRegistry = {
70 INFORMATION: InformationStep,
71 CUSTOM_COMPONENT: UserProfileFormStep, // Map 'CUSTOM_COMPONENT' to your form
72 // You'd add components for 'SINGLE_CHOICE', 'MULTIPLE_CHOICE', etc. here
73 // For example, you could defined a one-off component for the 'select-plan' id step
74 // 'select-plan': SelectPlanStep, // Example for a custom step
75};
Then, add it to your OnboardingProvider
:
1import { stepComponentRegistry } from "@/config/stepRegistry"
2// In your OnboardingWrapper
3return (
4 <OnboardingProvider
5 componentRegistry={stepComponentRegistry}
6 >
7 {children}
8 </OnboardingProvider>
9 );
And finally, we provide our Onboarding UI:
1// components/OnboardingUI.tsx
2import React from "react";
3import {
4 useOnboarding,
5} from "@onboardjs/react";
6import type { OnboardingContext } from "@onboardjs/core";
7
8export default function OnboardingUI() {
9 const { engine, state, next, previous, isCompleted, currentStep, renderStep, error } =
10 useOnboarding<MyAppContext>();
11
12 if (!engine || !state) {
13 return <div className="p-4">Loading onboarding...</div>;
14 }
15
16 if (error) {
17 return (
18 <div className="p-4 text-red-500">
19 Error: {error.message} (Please check console for details)
20 </div>
21 );
22 }
23
24 if (currentStep === null || isCompleted) {
25 return (
26 <div className="p-8 text-center bg-green-50 rounded-lg">
27 <h2 className="text-3xl font-bold text-green-700">
28 Onboarding Complete!
29 </h2>
30 <p className="text-gray-700 mt-4">
31 Thanks for walking through the flow. Check your console for the final
32 context!
33 </p>
34 <button
35 onClick={() => engine.reset()}
36 className="mt-6 px-6 py-3 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
37 >
38 Reset Onboarding
39 </button>
40 </div>
41 );
42 }
43
44 const { isLastStep, canGoPrevious } = state;
45
46
47 return (
48 <div className="p-8 bg-white rounded-lg shadow-xl max-w-md mx-auto my-10">
49 <h3 className="text-xl font-semibold mb-6">
50 Step: {String(currentStep?.id)} ({currentStep?.type})
51 </h3>
52 <div className="mb-6">
53 {renderStep()}
54 </div>
55 <div className="flex justify-between mt-8">
56 <button
57 onClick={() => previous()}
58 disabled={!canGoPrevious}
59 className="px-6 py-3 bg-gray-300 text-gray-800 rounded-md disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-400 transition-colors"
60 >
61 Previous
62 </button>
63 <button
64 onClick={() => next()}
65 className="px-6 py-3 bg-blue-600 text-white rounded-md disabled:opacity-50 disabled:cursor-not-allowed hover:bg-blue-700 transition-colors"
66 >
67 {isLastStep ? "Finish" : "Next"}
68 </button>
69 </div>
70 <div className="mt-4 text-sm text-center text-gray-500">
71 Current flow data:{" "}
72 <pre className="bg-gray-100 p-2 rounded text-xs mt-2 overflow-x-auto">
73 {JSON.stringify(state.context.flowData, null, 2)}
74 </pre>
75 </div>
76 </div>
77 );
78}
The custom step components and the Onboarding UI is where YOU shine. These are the components where you can realise your beautiful design!
Next Steps: Beyond the Basics
You've now got a functional Next.js onboarding flow! But OnboardJS offers much more:
- Conditional Steps: Use the condition property on any step to dynamically include or skip it based on your context.
- Plugins for Persistence & Analytics: Integrate seamlessly with your backend or analytics tools. Check out our dedicated
@onboardjs/supabase-plugin
and@onboardjs/posthog-plugin
for automated data handling. - Advanced Step Types: Explore CHECKLIST, MULTIPLE_CHOICE, and SINGLE_CHOICE steps for common onboarding patterns, or define even more CUSTOM_COMPONENT types.
- Custom Context: Extend the OnboardingContext to store any global data your flow needs, making it accessible across all steps.
- Error Handling: Leverage the built-in error handling and error state from
useOnboarding
to provide graceful fallbacks.
Conclusion: Build Better Onboarding, Faster
Building a compelling React onboarding flow doesn't have to be a drag. OnboardJS empowers you to create dynamic, data-driven user journeys with a clear separation of concerns, robust features, and excellent developer experience. By handling the complex orchestration, OnboardJS lets you focus on what truly matters: designing an intuitive and effective first impression for your users.
Ready to build your next Next.js onboarding?
- Explore the live demo: https://onboardjs.com/
- Get the source code and ⭐ us on GitHub: https://github.com/Somafet/onboardjs
- Join our Discord community
What challenges have you faced building Next.js onboarding flows, and how could a tool like OnboardJS help you overcome them? Tell me on Discord!