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
npm 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
// src/onboarding-config.ts
// Import your actual React components here
import ProfileSetupForm from './components/ProfileSetupForm';
import FinishStepComponent from './components/FinishStepComponent';

// Define your onboarding flow steps
export const steps: OnboardingStep[] = [
  {
    id: 'welcome',
    type: 'CUSTOM_COMPONENT',
    payload: {
      componentKey: 'ProfileSetupForm', // Maps to our component registry
      title: 'Welcome to Our App!', // Example custom payload data
      description: 'Let\'s get your profile set up quickly.',
    },
  },
  {
    id: 'finish',
    type: 'CUSTOM_COMPONENT',
    payload: {
      componentKey: 'FinishStepComponent', // Maps to our finish component
      title: 'Setup Complete!',
      message: 'You\'re all set to explore our amazing features!',
    },
    nextStep: null, // Critical: 'null' indicates the end of the flow
  },
];

// Define your component registry to map componentKeys to actual React components
// This tells OnboardJS which UI component to render for each custom step.
export const componentRegistry: StepComponentRegistry = {
  ProfileSetupForm: ProfileSetupForm,
  FinishStepComponent: FinishStepComponent,
};

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
// src/components/ProfileSetupForm.tsx
"use client"; // Required for Next.js App Router client components
import React from "react";
import { useOnboarding, StepComponentProps } from "@onboardjs/react"; // <-- Essential hook for navigation

interface ProfileSetupPayload {
  title: string;
  description: string;
}

const ProfileSetupForm: React.FC<StepComponentProps<ProfileSetupPayload>> = ({
  payload, // Access the payload data passed from the engine
  coreContext, // Access global onboarding data here if needed
}) => {
  const { next, isLoading, state, updateContext } = useOnboarding(); // Get navigation functions and current state

  const updateName = (newName: string) => {
    // Then, you update the context here
    updateContext({ flowData: { profileName: newName } });
  };

  return (
    <div>
      <h2>{payload.title}</h2>
      <p>{payload.description}</p>

      <label htmlFor="name-input">Your Name:</label>
      <input
        id="name-input"
        type="text"
        defaultValue={coreContext?.flowData?.profileName || ""}
        onChange={(e) => updateName(e.target.value)}
        placeholder="Enter your name"
      />

      <button
        onClick={() => next()} // Calls the engine's 'next' function
        disabled={isLoading || !state?.canGoNext} // Disable if loading or if step isn't valid
      >
        {isLoading ? "Loading..." : "Next"}
      </button>
    </div>
  );
};

export 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
// src/components/FinishStepComponent.tsx
"use client"; // Required for Next.js App Router client components

import React from "react";
import { StepComponentProps, useOnboarding } from "@onboardjs/react";
// No need for useOnboarding here if no navigation action is taken on this final step

interface FinishPayload {
  title: string;
  message: string;
}

const FinishStepComponent: React.FC<StepComponentProps<FinishPayload>> = ({
  payload,
  coreContext,
}) => {
  const { next } = useOnboarding();
  // You can access all collected data from coreContext.flowData
  const finalUserName = coreContext.flowData.profileName || "Guest"; // Use 'profileName' collected from previous step

  return (
    <div
      style={{
        padding: "20px",
        border: "1px solid #eee",
        borderRadius: "8px",
        maxWidth: "400px",
        margin: "auto",
      }}
    >
      <h2>{payload.title}</h2>
      <p>{payload.message}</p>
      <p>Welcome, {finalUserName}!</p>
      <p>
        Your collected data:
        <pre
          style={{
            backgroundColor: "#f0f0f0",
            padding: "10px",
            borderRadius: "4px",
            overflowX: "auto",
            fontSize: "0.9em",
          }}
        >
          {JSON.stringify(coreContext.flowData, null, 2)}
        </pre>
      </p>
      {/* The Finish button */}
      <button
        onClick={() => {
          // Finish the onboarding flow by calling next()
          next();
        }}
      >
        Finish
      </button>
    </div>
  );
};

export 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
// ./components/OnboardingUI.tsx
"use client"; // REQUIRED for client-side hooks like useOnboarding and useRouter

import React from 'react';
import { useOnboarding } from "@onboardjs/react";

// Create a simple wrapper component to manage global UI and navigation
export default function OnboardingUI() {
  const { state, isLoading, next, previous, skip, reset, renderStep } = useOnboarding();

  // Basic Loading/Completion/Error States for brevity in this guide
  if (!state) {
    return <div style={{ textAlign: 'center', padding: '20px' }}>Loading onboarding...</div>;
  }
  if (state.error) {
    return (
      <div style={{ textAlign: 'center', padding: '20px', color: 'red' }}>
        Error: {state.error.message} <button onClick={() => reset()} style={{ marginLeft: '10px' }}>Reset</button>
      </div>
    );
  }
  if (state.isCompleted) {
    return (
      <div style={{ textAlign: 'center', padding: '20px' }}>
        <h2>Onboarding Completed! 🎉</h2>
        <p>You're all set. Redirecting to dashboard...</p>
      </div>
    );
  }

  return (
    <div style={{ maxWidth: '600px', margin: '20px auto', padding: '20px', border: '1px solid #ccc', borderRadius: '8px' }}>
      {/* The function that renders your current step's UI */}
      {renderStep()}

      {/* (Optional) Basic Navigation Buttons */}
      <div style={{ marginTop: '20px', display: 'flex', justifyContent: 'space-between' }}>
        {state.canGoPrevious && (
          <button onClick={previous} disabled={isLoading} style={{ padding: '8px 15px', cursor: 'pointer' }}>
            Back
          </button>
        )}
        {state.isSkippable && (
          <button onClick={skip} disabled={isLoading} style={{ padding: '8px 15px', cursor: 'pointer', marginLeft: 'auto' }}>
            Skip
          </button>
        )}
        {state.canGoNext && (
          <button onClick={next} disabled={isLoading || !state.canGoNext} style={{ padding: '8px 15px', cursor: 'pointer', backgroundColor: '#0070f3', color: 'white', border: 'none' }}>
            Next
          </button>
        )}
        {state.isLastStep && !state.isCompleted && (
          <button onClick={next} disabled={isLoading} style={{ padding: '8px 15px', cursor: 'pointer', backgroundColor: '#28a745', color: 'white', border: 'none' }}>
            Finish
          </button>
        )}
      </div>
    </div>
  );
}

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
// src/app/layout.tsx (for Next.js App Router example)
"use client"; // REQUIRED for client-side hooks like useOnboarding and useRouter

import React from "react";
import { OnboardingProvider } from "@onboardjs/react";
import { steps, componentRegistry } from "./onboarding-config";
import { useRouter } from "next/navigation"; // Import useRouter for redirection example

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const router = useRouter(); // Initialize router if you want to redirect after flow completion

  return (
    <html lang="en">
      <body>
        <OnboardingProvider
          steps={steps}
          componentRegistry={componentRegistry} // Link your UI components
          onFlowComplete={(context) => {
            console.log("Onboarding flow completed!", context);
            alert(
              `Onboarding complete for ${
                context.flowData.profileName || "user"
              }!`
            );
            // Example: Redirect the user to their dashboard after onboarding
            router.push("/dashboard"); // Make sure you have a /dashboard page!
          }}
          onStepChange={(newStep, oldStep, context) => {
            console.log("Step changed:", oldStep?.id, "->", newStep?.id);
            // You could use this to send analytics events, update UI, etc.
          }}
          localStoragePersistence={{ key: "myAppOnboardingState" }} // Magical persistence!
          initialStepId="welcome" // Optional: Specify starting step if not the first in array
        >
          <main>
            {/* You can render your application UI here */}
            <YourApplicationUI />
          </main>
        </OnboardingProvider>
      </body>
    </html>
  );
}

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
npm 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?