Monday, June 23, 2025

Stop Wasting Time: Build Your First Onboarding Flow with OnboardJS in Under 10 Minutes

Soma Somorjai
onboardjs demo walkthrough gif video

I Hate When Building Simple Onboarding Feels Like a Whole New App

As a developer, I've spent countless hours wrestling with custom onboarding logic. The state management, the routing, the persistence, the conditional steps... it's a headache most libraries just ignore, leaving you to piece together a complex state machine from scratch.

You're trying to ship a great product, but instead, you're deep in the weeds of boilerplate code, managing all the possible states your app could be in as users navigate back and forth, or worse, worrying about losing their progress on a simple page refresh.

The truth is: You shouldn't have to build a complex state machine for every single onboarding flow. OnboardJS does it for you.

In this post, you'll learn how to leverage OnboardJS to create robust, maintainable onboarding experiences in minutes, not days. We'll build a simple two-step flow from scratch using Next.js and React.

Introducing OnboardJS: The Headless Advantage

OnboardJS is a headless, type-safe, and extensible engine designed specifically for managing multi-step user flows. What does "headless" mean? It means OnboardJS gives you the powerful "brain" to manage your flow's state, logic, and persistence, while you provide the "beauty" – your own React UI components.

This separation is crucial:

  • Your UI, Your Rules: Use any component library (Tailwind, Chakra UI, Shadcn UI, custom components!).
  • Built-in Logic: Handle complex navigation, conditional steps, and data collection effortlessly.
  • Automatic Persistence: Store user progress seamlessly, preventing frustrating restarts.
  • Type-Safe: Catch errors early with full TypeScript support, making your flows more robust.

Combined with the @onboardjs/react package, it becomes incredibly intuitive to integrate OnboardJS into your Next.js application.

Hands-On: Building Your First Flow (The 10-Minute Challenge!)

Ready to see how easy it is? We'll create a simple two-step onboarding flow: a "Welcome" step to collect a user's name, and a "Finish" step to display collected data.

No boilerplate, no complex state machines. Just define your steps, map your UI, and let OnboardJS handle the rest.

4.1. Project Setup

First, you will need a Next.js app (or use your existing one).
Then, install OnboardJS:

sh
1 lines
1npm install @onboardjs/core @onboardjs/react

4.2. Define Your Onboarding Steps & Components

OnboardJS steps define the flow's logic, not the UI. For our quick start, we'll use CUSTOM_COMPONENT steps, which let you bring any React component into your flow.

Create a file like src/onboarding-config.ts (or onboarding-config.tsx if your components are in the same file) and add the following:

tsx
34 lines
1// src/onboarding-config.ts
2// Import your actual React components here
3import ProfileSetupForm from './components/ProfileSetupForm';
4import FinishStepComponent from './components/FinishStepComponent';
5
6// Define your onboarding flow steps
7export const steps: OnboardingStep[] = [
8  {
9    id: 'welcome',
10    type: 'CUSTOM_COMPONENT',
11    payload: {
12      componentKey: 'ProfileSetupForm', // Maps to our component registry
13      title: 'Welcome to Our App!', // Example custom payload data
14      description: 'Let\'s get your profile set up quickly.',
15    },
16  },
17  {
18    id: 'finish',
19    type: 'CUSTOM_COMPONENT',
20    payload: {
21      componentKey: 'FinishStepComponent', // Maps to our finish component
22      title: 'Setup Complete!',
23      message: 'You\'re all set to explore our amazing features!',
24    },
25    nextStep: null, // Critical: 'null' indicates the end of the flow
26  },
27];
28
29// Define your component registry to map componentKeys to actual React components
30// This tells OnboardJS which UI component to render for each custom step.
31export const componentRegistry: StepComponentRegistry = {
32  ProfileSetupForm: ProfileSetupForm,
33  FinishStepComponent: FinishStepComponent,
34};

The payload object allows you to pass any custom data your React component needs (like a title or description). The componentKey is how OnboardJS knows which of your React components to render for this step type.

4.3. Create Your React Components

Now, let's create the actual React components that OnboardJS will render. These components receive payload (your custom data), coreContext (the global onboarding data), and onDataChange (a function to update collected data).

src/components/ProfileSetupForm.tsx:

tsx
46 lines
1// src/components/ProfileSetupForm.tsx
2"use client"; // Required for Next.js App Router client components
3import React from "react";
4import { useOnboarding, StepComponentProps } from "@onboardjs/react"; // <-- Essential hook for navigation
5
6interface ProfileSetupPayload {
7  title: string;
8  description: string;
9}
10
11const ProfileSetupForm: React.FC<StepComponentProps<ProfileSetupPayload>> = ({
12  payload, // Access the payload data passed from the engine
13  coreContext, // Access global onboarding data here if needed
14}) => {
15  const { next, isLoading, state, updateContext } = useOnboarding(); // Get navigation functions and current state
16
17  const updateName = (newName: string) => {
18    // Then, you update the context here
19    updateContext({ flowData: { profileName: newName } });
20  };
21
22  return (
23    <div>
24      <h2>{payload.title}</h2>
25      <p>{payload.description}</p>
26
27      <label htmlFor="name-input">Your Name:</label>
28      <input
29        id="name-input"
30        type="text"
31        defaultValue={coreContext?.flowData?.profileName || ""}
32        onChange={(e) => updateName(e.target.value)}
33        placeholder="Enter your name"
34      />
35
36      <button
37        onClick={() => next()} // Calls the engine's 'next' function
38        disabled={isLoading || !state?.canGoNext} // Disable if loading or if step isn't valid
39      >
40        {isLoading ? "Loading..." : "Next"}
41      </button>
42    </div>
43  );
44};
45
46export default ProfileSetupForm;

In ProfileSetupForm.tsx, we use the useOnboarding hook to get next() (to advance the flow), isLoading (to provide visual feedback), and state (to check if canGoNext).

src/components/FinishStepComponent.tsx:

tsx
61 lines
1// src/components/FinishStepComponent.tsx
2"use client"; // Required for Next.js App Router client components
3
4import React from "react";
5import { StepComponentProps, useOnboarding } from "@onboardjs/react";
6// No need for useOnboarding here if no navigation action is taken on this final step
7
8interface FinishPayload {
9  title: string;
10  message: string;
11}
12
13const FinishStepComponent: React.FC<StepComponentProps<FinishPayload>> = ({
14  payload,
15  coreContext,
16}) => {
17  const { next } = useOnboarding();
18  // You can access all collected data from coreContext.flowData
19  const finalUserName = coreContext.flowData.profileName || "Guest"; // Use 'profileName' collected from previous step
20
21  return (
22    <div
23      style={{
24        padding: "20px",
25        border: "1px solid #eee",
26        borderRadius: "8px",
27        maxWidth: "400px",
28        margin: "auto",
29      }}
30    >
31      <h2>{payload.title}</h2>
32      <p>{payload.message}</p>
33      <p>Welcome, {finalUserName}!</p>
34      <p>
35        Your collected data:
36        <pre
37          style={{
38            backgroundColor: "#f0f0f0",
39            padding: "10px",
40            borderRadius: "4px",
41            overflowX: "auto",
42            fontSize: "0.9em",
43          }}
44        >
45          {JSON.stringify(coreContext.flowData, null, 2)}
46        </pre>
47      </p>
48      {/* The Finish button */}
49      <button
50        onClick={() => {
51          // Finish the onboarding flow by calling next()
52          next();
53        }}
54      >
55        Finish
56      </button>
57    </div>
58  );
59};
60
61export default FinishStepComponent;

The FinishStepComponent.tsx demonstrates how easily you can access all collected data from coreContext.flowData, allowing you to personalize the final step or display a summary.

4.4. Render your Onboarding UI

Now combining these previous steps and components will be your Onboarding UI.

This component will allow you to render the current step automatically and wrap your steps in an optional frame.

tsx
61 lines
1// ./components/OnboardingUI.tsx
2"use client"; // REQUIRED for client-side hooks like useOnboarding and useRouter
3
4import React from 'react';
5import { useOnboarding } from "@onboardjs/react";
6
7// Create a simple wrapper component to manage global UI and navigation
8export default function OnboardingUI() {
9  const { state, isLoading, next, previous, skip, reset, renderStep } = useOnboarding();
10
11  // Basic Loading/Completion/Error States for brevity in this guide
12  if (!state) {
13    return <div style={{ textAlign: 'center', padding: '20px' }}>Loading onboarding...</div>;
14  }
15  if (state.error) {
16    return (
17      <div style={{ textAlign: 'center', padding: '20px', color: 'red' }}>
18        Error: {state.error.message} <button onClick={() => reset()} style={{ marginLeft: '10px' }}>Reset</button>
19      </div>
20    );
21  }
22  if (state.isCompleted) {
23    return (
24      <div style={{ textAlign: 'center', padding: '20px' }}>
25        <h2>Onboarding Completed! 🎉</h2>
26        <p>You're all set. Redirecting to dashboard...</p>
27      </div>
28    );
29  }
30
31  return (
32    <div style={{ maxWidth: '600px', margin: '20px auto', padding: '20px', border: '1px solid #ccc', borderRadius: '8px' }}>
33      {/* The function that renders your current step's UI */}
34      {renderStep()}
35
36      {/* (Optional) Basic Navigation Buttons */}
37      <div style={{ marginTop: '20px', display: 'flex', justifyContent: 'space-between' }}>
38        {state.canGoPrevious && (
39          <button onClick={previous} disabled={isLoading} style={{ padding: '8px 15px', cursor: 'pointer' }}>
40            Back
41          </button>
42        )}
43        {state.isSkippable && (
44          <button onClick={skip} disabled={isLoading} style={{ padding: '8px 15px', cursor: 'pointer', marginLeft: 'auto' }}>
45            Skip
46          </button>
47        )}
48        {state.canGoNext && (
49          <button onClick={next} disabled={isLoading || !state.canGoNext} style={{ padding: '8px 15px', cursor: 'pointer', backgroundColor: '#0070f3', color: 'white', border: 'none' }}>
50            Next
51          </button>
52        )}
53        {state.isLastStep && !state.isCompleted && (
54          <button onClick={next} disabled={isLoading} style={{ padding: '8px 15px', cursor: 'pointer', backgroundColor: '#28a745', color: 'white', border: 'none' }}>
55            Finish
56          </button>
57        )}
58      </div>
59    </div>
60  );
61}

Here, we've created a simple OnboardingUI component that uses useOnboarding() to get the current state and navigation functions. The {renderStep()} is the core, telling OnboardJS to render the correct React component you defined in your componentRegistry for the active step. We also added basic 'Next', 'Back', 'Skip', and 'Finish' buttons, leveraging state.canGoNext, state.canGoPrevious, and state.isSkippable for intelligent button control. This simple wrapper centralizes your onboarding UI logic.

4.5. Integrate OnboardJS into Your React App

Finally, wrap your application with the OnboardingProvider. This makes the OnboardJS engine available throughout your app via the useOnboarding hook, and tells it which steps to manage.

In your main application file (e.g., src/app/layout.tsx for Next.js App Router, or src/App.tsx for a Create React App). Remember to include "use client"; at the top if you're using Next.js App Router and this is a Server Component by default.

You can also wrap your OnboardingProvider in a client component first as to avoid making the whole layout.tsx a client component!

tsx
47 lines
1// src/app/layout.tsx (for Next.js App Router example)
2"use client"; // REQUIRED for client-side hooks like useOnboarding and useRouter
3
4import React from "react";
5import { OnboardingProvider } from "@onboardjs/react";
6import { steps, componentRegistry } from "./onboarding-config";
7import { useRouter } from "next/navigation"; // Import useRouter for redirection example
8
9export default function RootLayout({
10  children,
11}: {
12  children: React.ReactNode;
13}) {
14  const router = useRouter(); // Initialize router if you want to redirect after flow completion
15
16  return (
17    <html lang="en">
18      <body>
19        <OnboardingProvider
20          steps={steps}
21          componentRegistry={componentRegistry} // Link your UI components
22          onFlowComplete={(context) => {
23            console.log("Onboarding flow completed!", context);
24            alert(
25              `Onboarding complete for ${
26                context.flowData.profileName || "user"
27              }!`
28            );
29            // Example: Redirect the user to their dashboard after onboarding
30            router.push("/dashboard"); // Make sure you have a /dashboard page!
31          }}
32          onStepChange={(newStep, oldStep, context) => {
33            console.log("Step changed:", oldStep?.id, "->", newStep?.id);
34            // You could use this to send analytics events, update UI, etc.
35          }}
36          localStoragePersistence={{ key: "myAppOnboardingState" }} // Magical persistence!
37          initialStepId="welcome" // Optional: Specify starting step if not the first in array
38        >
39          <main>
40            {/* You can render your application UI here */}
41            <YourApplicationUI />
42          </main>
43        </OnboardingProvider>
44      </body>
45    </html>
46  );
47}

The OnboardingProvider is your central hub. It takes your steps and componentRegistry to render the correct UI. Notice onFlowComplete and onStepChange for powerful lifecycle hooks, and localStoragePersistence for automatic state saving – a common pain point solved with a single line!

4.5. Run Your App!

Fire up your development server:

sh
1 lines
1npm run dev

Navigate to your app in the browser. You should see your 'Welcome' step! Try filling in your name and clicking 'Next'.

Then, refresh the page – your progress will be saved automatically thanks to localStoragePersistence! If you go back to the app, you'll land directly on the 'Finish' step.

Final thoughts

You've just built a fully functional, stateful onboarding flow in minutes, not hours or days, without a single line of state management boilerplate. This is just the beginning of what OnboardJS can do:

  • Conditional Logic: Implement dynamic flows where steps appear or change based on user input or coreContext values, using condition and dynamic nextStep/previousStep functions.
  • Deep Customization: Use updateContext to modify the global flow data dynamically.
  • The Powerful Plugin System: Extend OnboardJS with custom integrations, like connecting to your backend for cross-device persistence.

Read more on how to integrate with Supabase to achieve a consistent cross-device onboarding flow!

Ready to build smarter onboarding experiences that delight users and save you development time?

Stop Wasting Time: Build Your First Onboarding Flow with OnboardJS in Under 10 Minutes - OnboardJS