About onboardjs-react
onboardjs-react is a React integration guide for building onboarding flows with OnboardJS. It explains setup and patterns for developers who need custom UI while delegating flow state and navigation to the library.
Key features
- 01React setup and package-manager detection
- 02Step components and provider configuration
- 03Onboarding hooks for rendering and navigation
- 04Progress indicators using step IDs
- 05Next.js client-side setup and lazy loading
- 06Local and custom-backend persistence
Use cases
- 01Adding onboarding flows to a React app
- 02Adapting onboarding setup for Next.js
- 03Tracking progress across onboarding steps
- 04Persisting onboarding state locally or through an API
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ onboardjs/ onboardjs-skills --skill onboardjs-reactAfter installing, type/onboardjs-reactin Claude Code to confirm it loaded.
Bundled files
References and scripts installed along with SKILL.md.
references/hooks-api.mdpatterns.mdstep-config.mdDetails
From the project READMEOnboardJS React Integration
OnboardJS is a headless library for building user onboarding experiences. You control the UI; OnboardJS handles flow logic, state, persistence, and navigation.
Before Starting
1. Verify React project - Check for package.json with React dependencies. If not a React project, inform the user.
2. Detect package manager - Check for lock files to determine the correct install command:
| Lock File | Package Manager | Install Command |
|---|---|---|
pnpm-lock.yaml | pnpm | pnpm add @onboardjs/core @onboardjs/react |
yarn.lock | yarn | yarn add @onboardjs/core @onboardjs/react |
bun.lockb | bun | bun add @onboardjs/core @onboardjs/react |
package-lock.json or none | npm | npm install @onboardjs/core @onboardjs/react |
3. Detect Next.js - Check for next.config.js, next.config.mjs, or next in dependencies. If Next.js, see Next.js Setup section.
Installation
# npm
npm install @onboardjs/core @onboardjs/react
# pnpm
pnpm add @onboardjs/core @onboardjs/react
# yarn
yarn add @onboardjs/core @onboardjs/react
# bun
bun add @onboardjs/core @onboardjs/react
Quick Setup
1. Define Steps with Components
// steps.tsx
import { OnboardingStep } from '@onboardjs/react'
import { WelcomeStep } from './components/WelcomeStep'
import { ProfileFormStep } from './components/ProfileFormStep'
import { CompleteStep } from './components/CompleteStep'
const steps: OnboardingStep[] = [
{
id: 'welcome',
component: WelcomeStep,
payload: { title: 'Welcome!', description: 'Let\'s get started' },
nextStep: 'profile'
},
{
id: 'profile',
component: ProfileFormStep,
nextStep: 'complete'
},
{
id: 'complete',
component: CompleteStep,
payload: { title: 'All done!' },
nextStep: null
}
]
2. Wrap with Provider
import { OnboardingProvider } from '@onboardjs/react'
import { steps } from './steps'
function App() {
return (
<OnboardingProvider
steps={steps}
onFlowComplete={(ctx) => console.log('Done!', ctx)}
>
<OnboardingUI />
</OnboardingProvider>
)
}
3. Use the Hook
import { useOnboarding } from '@onboardjs/react'
function OnboardingUI() {
const { renderStep, next, previous, state, loading } = useOnboarding()
if (loading.isHydrating) return <Spinner />
if (state?.isCompleted) return <CompletedScreen />
return (
<div>
{renderStep()}
<div>
<button onClick={previous} disabled={state?.isFirstStep}>Back</button>
<button onClick={() => next()} disabled={!state?.canGoNext}>
{state?.isLastStep ? 'Finish' : 'Next'}
</button>
</div>
</div>
)
}
Step Indicator / Progress
Important: The state object does NOT have a steps array. Use currentStepNumber and totalSteps, or calculate from step IDs.
Option 1: Use Built-in Properties (if available)
function OnboardingUI() {
const { state, renderStep } = useOnboarding()
return (
<div>
{state?.currentStepNumber && state?.totalSteps && (
<div>Step {state.currentStepNumber} of {state.totalSteps}</div>
)}
<progress
value={state?.currentStepNumber ?? 0}
max={state?.totalSteps ?? 1}
/>
{renderStep()}
</div>
)
}
Option 2: Calculate from Step IDs (recommended)
For reliable progress tracking, define step IDs once and calculate the index:
// steps.tsx - export your step IDs
export const STEP_IDS = ['welcome', 'profile', 'preferences', 'complete'] as const
export const steps: OnboardingStep[] = [
{ id: 'welcome', component: WelcomeStep, nextStep: 'profile' },
{ id: 'profile', component: ProfileStep, nextStep: 'preferences' },
{ id: 'preferences', component: PreferencesStep, nextStep: 'complete' },
{ id: 'complete', component: CompleteStep, nextStep: null }
]
// OnboardingUI.tsx
import { STEP_IDS } from './steps'
function OnboardingUI() {
const { state, renderStep } = useOnboarding()
const currentIndex = STEP_IDS.findIndex(id => id === state?.currentStep?.id)
const currentStepNumber = currentIndex + 1
const totalSteps = STEP_IDS.length
return (
<div>
<div>Step {currentStepNumber} of {totalSteps}</div>
<progress value={currentStepNumber} max={totalSteps} />
{renderStep()}
</div>
)
}
Step Indicator Component
interface StepIndicatorProps {
stepIds: readonly string[]
currentStepId: string | undefined
}
function StepIndicator({ stepIds, currentStepId }: StepIndicatorProps) {
const currentIndex = stepIds.findIndex(id => id === currentStepId)
return (
<div className="flex gap-2">
{stepIds.map((id, index) => (
<div
key={id}
className={`w-3 h-3 rounded-full ${
index < currentIndex ? 'bg-green-500' :
index === currentIndex ? 'bg-blue-500' :
'bg-gray-300'
}`}
/>
))}
</div>
)
}
// Usage
<StepIndicator stepIds={STEP_IDS} currentStepId={state?.currentStep?.id} />
Step Component Pattern
import { StepComponentProps } from '@onboardjs/react'
interface ProfilePayload {
title: string
fields: string[]
}
const ProfileFormStep: React.FC<StepComponentProps<ProfilePayload>> = ({
payload,
context,
onDataChange,
initialData
}) => {
const [name, setName] = useState(initialData?.name || '')
const handleChange = (value: string) => {
setName(value)
onDataChange?.({ name: value }, value.length > 0)
}
return (
<div>
<h2>{payload.title}</h2>
<input value={name} onChange={(e) => handleChange(e.target.value)} />
</div>
)
}
Next.js Setup
OnboardJS uses React hooks and browser APIs, requiring client-side rendering in Next.js App Router.
Step Components - Add "use client"
All step components must be client components:
// components/WelcomeStep.tsx
'use client'
import { StepComponentProps } from '@onboardjs/react'
export const WelcomeStep: React.FC<StepComponentProps> = ({ payload }) => {
return <h1>{payload.title}</h1>
}
Provider Wrapper - Add "use client"
Create a client wrapper for the provider:
// components/OnboardingWrapper.tsx
'use client'
import { OnboardingProvider } from '@onboardjs/react'
import { steps } from './steps'
export function OnboardingWrapper({ children }: { children: React.ReactNode }) {
return (
<OnboardingProvider
steps={steps}
onFlowComplete={(ctx) => console.log('Done!', ctx)}
>
{children}
</OnboardingProvider>
)
}
Use in Page (App Router)
// app/onboarding/page.tsx
import { OnboardingWrapper } from '@/components/OnboardingWrapper'
import { OnboardingUI } from '@/components/OnboardingUI'
export default function OnboardingPage() {
return (
<OnboardingWrapper>
<OnboardingUI />
</OnboardingWrapper>
)
}
Dynamic Import (Optional - for code splitting)
Use dynamic imports to reduce initial bundle size:
// app/onboarding/page.tsx
import dynamic from 'next/dynamic'
const OnboardingWrapper = dynamic(
() => import('@/components/OnboardingWrapper').then(mod => mod.OnboardingWrapper),
{
ssr: false,
loading: () => <div>Loading onboarding...</div>
}
)
export default function OnboardingPage() {
return <OnboardingWrapper><OnboardingUI /></OnboardingWrapper>
}
Dynamic Step Components (Optional - for large flows)
Lazy-load step components to reduce bundle size:
// steps.tsx
'use client'
import dynamic from 'next/dynamic'
import { OnboardingStep } from '@onboardjs/react'
const WelcomeStep = dynamic(() => import('./components/WelcomeStep').then(m => m.WelcomeStep))
const ProfileStep = dynamic(() => import('./components/ProfileStep').then(m => m.ProfileStep))
const CompleteStep = dynamic(() => import('./components/CompleteStep').then(m => m.CompleteStep))
export const steps: OnboardingStep[] = [
{ id: 'welcome', component: WelcomeStep, nextStep: 'profile' },
{ id: 'profile', component: ProfileStep, nextStep: 'complete' },
{ id: 'complete', component: CompleteStep, nextStep: null }
]
Pages Router (Legacy)
For Next.js Pages Router, no "use client" needed but disable SSR:
// pages/onboarding.tsx
import dynamic from 'next/dynamic'
const OnboardingFlow = dynamic(
() => import('@/components/OnboardingFlow'),
{ ssr: false }
)
export default function OnboardingPage() {
return <OnboardingFlow />
}
Persistence
localStorage (Simple)
<OnboardingProvider
steps={steps}
localStoragePersistence={{ key: 'onboarding_v1', ttl: 604800000 }}
>
Custom Backend
<OnboardingProvider
steps={steps}
customOnDataLoad={async () => await fetchFromAPI()}
customOnDataPersist={async (ctx) => await saveToAPI(ctx)}
customOnClearPersistedData={async () => await clearAPI()}
>
Conditional Navigation
import { RoleSelectStep } from './components/RoleSelectStep'
import { AdminSetupStep } from './components/AdminSetupStep'
import { UserSetupStep } from './components/UserSetupStep'
{
id: 'role-select',
component: RoleSelectStep,
payload: {
options: [
{ value: 'admin', label: 'Admin' },
{ value: 'user', label: 'User' }
]
},
nextStep: (ctx) => ctx.flowData.role === 'admin' ? 'admin-setup' : 'user-setup'
}
Conditional Step Visibility
{
id: 'admin-setup',
component: AdminSetupStep,
condition: (ctx) => ctx.flowData.role === 'admin'
}
Advanced: See References
- references/step-config.md - Full step configuration options
- references/hooks-api.md - Complete useOnboarding API
- references/patterns.md - Advanced patterns (validation, events, plugins, Next.js)