Build Your First Onboarding Flow with OnboardJS in Under 10 Minutes

5 hours ago 1

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:

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:

import ProfileSetupForm from './components/ProfileSetupForm'; import FinishStepComponent from './components/FinishStepComponent'; export const steps: OnboardingStep[] = [ { id: 'welcome', type: 'CUSTOM_COMPONENT', payload: { componentKey: 'ProfileSetupForm', title: 'Welcome to Our App!', description: 'Let\'s get your profile set up quickly.', }, }, { id: 'finish', type: 'CUSTOM_COMPONENT', payload: { componentKey: 'FinishStepComponent', title: 'Setup Complete!', message: 'You\'re all set to explore our amazing features!', }, nextStep: null, }, ]; 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:

"use client"; import React from "react"; import { useOnboarding, StepComponentProps } from "@onboardjs/react"; interface ProfileSetupPayload { title: string; description: string; } const ProfileSetupForm: React.FC<StepComponentProps<ProfileSetupPayload>> = ({ payload, coreContext, }) => { const { next, isLoading, state, updateContext } = useOnboarding(); const updateName = (newName: string) => { 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()} disabled={isLoading || !state?.canGoNext} > {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:

"use client"; import React from "react"; import { StepComponentProps, useOnboarding } from "@onboardjs/react"; interface FinishPayload { title: string; message: string; } const FinishStepComponent: React.FC<StepComponentProps<FinishPayload>> = ({ payload, coreContext, }) => { const { next } = useOnboarding(); const finalUserName = coreContext.flowData.profileName || "Guest"; 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> {} <button onClick={() => { 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.

"use client"; import React from 'react'; import { useOnboarding } from "@onboardjs/react"; export default function OnboardingUI() { const { state, isLoading, next, previous, skip, reset, renderStep } = useOnboarding(); 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' }}> {} {renderStep()} {} <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!

"use client"; import React from "react"; import { OnboardingProvider } from "@onboardjs/react"; import { steps, componentRegistry } from "./onboarding-config"; import { useRouter } from "next/navigation"; export default function RootLayout({ children, }: { children: React.ReactNode; }) { const router = useRouter(); return ( <html lang="en"> <body> <OnboardingProvider steps={steps} componentRegistry={componentRegistry} onFlowComplete={(context) => { console.log("Onboarding flow completed!", context); alert( `Onboarding complete for ${ context.flowData.profileName || "user" }!` ); router.push("/dashboard"); }} onStepChange={(newStep, oldStep, context) => { console.log("Step changed:", oldStep?.id, "->", newStep?.id); }} localStoragePersistence={{ key: "myAppOnboardingState" }} initialStepId="welcome" > <main> {} <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:

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?

Read Entire Article