Monday, June 23, 2025
Stop Wasting Time: Build Your First Onboarding Flow with OnboardJS in Under 10 Minutes


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:
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:
// 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:
// 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:
// 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.
// ./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!
// 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:
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, usingcondition
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?