Onboarding Flow
Complete user onboarding flow with persistent progress, conditional steps, checklists, and tour completion callbacks
Onboarding Flow
A production-ready onboarding tour that persists progress, shows only to new users, and handles conditional steps.
What You'll Build
This example creates an onboarding flow that:
- Only shows to users who haven't completed it
- Remembers progress if the user leaves mid-tour
- Offers a "Don't show again" option
- Includes conditional steps based on user role
- Tracks completion for analytics
Complete Code
'use client';
import { useEffect, useState } from 'react';
import {
Tour,
TourStep,
TourCard,
TourCardHeader,
TourCardContent,
TourCardFooter,
TourOverlay,
TourProgress,
TourNavigation,
TourClose,
useTour,
usePersistence,
} from '@tour-kit/react';
export default function OnboardingFlow() {
const persistence = usePersistence({
storage: 'localStorage',
keyPrefix: 'myapp-onboarding',
});
const [shouldShow, setShouldShow] = useState(false);
const [isLoading, setIsLoading] = useState(true);
// Check if user has already completed or dismissed onboarding
useEffect(() => {
const completed = persistence.getCompletedTours().includes('onboarding');
const dontShow = persistence.getDontShowAgain('onboarding');
setShouldShow(!completed && !dontShow);
setIsLoading(false);
}, [persistence]);
if (isLoading) return <AppContent />;
return (
<Tour
id="onboarding"
autoStart={shouldShow}
onComplete={() => {
persistence.markCompleted('onboarding');
// Track analytics
analytics.track('onboarding_completed');
}}
onSkip={(stepIndex) => {
persistence.markSkipped('onboarding');
persistence.saveStep('onboarding', stepIndex);
analytics.track('onboarding_skipped', { step: stepIndex });
}}
onStepChange={(step, index) => {
// Save progress so user can resume
persistence.saveStep('onboarding', index);
}}
>
{/* Welcome step */}
<TourStep
target="#app-header"
title="Welcome to MyApp!"
content="We're excited to have you here. Let's take a quick tour to help you get started."
placement="bottom"
/>
{/* Main features */}
<TourStep
target="#dashboard-widget"
title="Your Dashboard"
content="This is your personal dashboard. You'll see key metrics and recent activity here."
placement="right"
/>
<TourStep
target="#create-button"
title="Create Your First Project"
content="Click here to create a new project. We have templates to help you get started quickly."
placement="bottom"
/>
{/* Settings */}
<TourStep
target="#settings-link"
title="Customize Your Experience"
content="Visit settings to personalize your workspace, notification preferences, and more."
placement="left"
/>
{/* Final step */}
<TourStep
target="#help-button"
title="Need Help?"
content="Click here anytime to access documentation, tutorials, and contact support. You're all set!"
placement="bottom"
/>
<TourOverlay />
<TourCard className="w-96">
<TourCardHeader>
<TourClose />
</TourCardHeader>
<TourCardContent />
<TourCardFooter>
<TourProgress variant="bar" />
<OnboardingNavigation persistence={persistence} />
</TourCardFooter>
</TourCard>
<AppContent />
</Tour>
);
}
// Custom navigation with "Don't show again" option
function OnboardingNavigation({
persistence,
}: {
persistence: ReturnType<typeof usePersistence>;
}) {
const { isFirstStep, isLastStep, next, prev, skip, complete } = useTour();
const [dontShow, setDontShow] = useState(false);
const handleSkip = () => {
if (dontShow) {
persistence.setDontShowAgain('onboarding', true);
}
skip();
};
const handleComplete = () => {
if (dontShow) {
persistence.setDontShowAgain('onboarding', true);
}
complete();
};
return (
<div className="space-y-3">
<div className="flex items-center gap-2 text-sm">
<input
type="checkbox"
id="dont-show"
checked={dontShow}
onChange={(e) => setDontShow(e.target.checked)}
className="rounded"
/>
<label htmlFor="dont-show" className="text-muted-foreground">
Don't show this again
</label>
</div>
<div className="flex justify-between">
<button
onClick={handleSkip}
className="text-sm text-muted-foreground hover:text-foreground"
>
Skip tour
</button>
<div className="flex gap-2">
{!isFirstStep && (
<button
onClick={prev}
className="px-3 py-1.5 text-sm border rounded-md hover:bg-accent"
>
Back
</button>
)}
<button
onClick={isLastStep ? handleComplete : next}
className="px-3 py-1.5 text-sm bg-primary text-primary-foreground rounded-md hover:bg-primary/90"
>
{isLastStep ? 'Get Started' : 'Next'}
</button>
</div>
</div>
</div>
);
}
function AppContent() {
return (
<div className="min-h-screen">
<header id="app-header" className="border-b p-4 flex justify-between items-center">
<h1 className="text-xl font-bold">MyApp</h1>
<nav className="flex gap-4">
<a id="settings-link" href="#" className="text-muted-foreground hover:text-foreground">
Settings
</a>
<button id="help-button" className="text-muted-foreground hover:text-foreground">
Help
</button>
</nav>
</header>
<div className="p-8 flex gap-8">
<main className="flex-1">
<div id="dashboard-widget" className="p-6 border rounded-lg mb-6">
<h2 className="font-semibold mb-2">Dashboard</h2>
<p className="text-muted-foreground">Your metrics and activity</p>
</div>
<button
id="create-button"
className="px-4 py-2 bg-primary text-primary-foreground rounded-md"
>
+ Create Project
</button>
</main>
</div>
</div>
);
}Key Features Explained
Persistence Configuration
const persistence = usePersistence({
storage: 'localStorage', // or 'sessionStorage', 'cookie'
keyPrefix: 'myapp-onboarding', // namespaces all keys
});The persistence hook stores:
- Completed tours - List of tour IDs the user finished
- Skipped tours - List of tour IDs the user skipped
- Last step - Where the user left off (for resuming)
- Don't show again - Per-tour preference flag
Conditional Display
useEffect(() => {
const completed = persistence.getCompletedTours().includes('onboarding');
const dontShow = persistence.getDontShowAgain('onboarding');
setShouldShow(!completed && !dontShow);
}, [persistence]);
<Tour autoStart={shouldShow}>Only shows the tour if:
- User hasn't completed it before
- User hasn't checked "Don't show again"
Progress Saving
<Tour
onStepChange={(step, index) => {
persistence.saveStep('onboarding', index);
}}
>Every time the user moves to a new step, their progress is saved. If they leave and return, you can resume from where they left off.
Resume From Last Step
const lastStep = persistence.getLastStep('onboarding');
<Tour
autoStart={shouldShow}
startAt={lastStep ?? 0} // Resume from saved step
>Analytics Integration
Track onboarding events for insights:
<Tour
onStart={() => {
analytics.track('onboarding_started', {
variant: 'default',
timestamp: Date.now(),
});
}}
onComplete={() => {
analytics.track('onboarding_completed', {
duration: calculateDuration(),
});
}}
onSkip={(stepIndex) => {
analytics.track('onboarding_skipped', {
step: stepIndex,
stepTitle: steps[stepIndex].title,
});
}}
onStepChange={(step, index) => {
analytics.track('onboarding_step_viewed', {
stepIndex: index,
stepId: step.id,
stepTitle: step.title,
});
}}
>Conditional Steps
Show different steps based on user role or features:
function OnboardingWithRoles({ userRole }: { userRole: 'admin' | 'user' }) {
return (
<Tour id="role-onboarding">
{/* Common steps for all users */}
<TourStep target="#dashboard" title="Dashboard" content="..." />
{/* Admin-only step */}
{userRole === 'admin' && (
<TourStep
target="#admin-panel"
title="Admin Panel"
content="As an admin, you have access to user management and settings."
/>
)}
{/* Common final step */}
<TourStep target="#help" title="Get Help" content="..." />
<TourOverlay />
<TourCard />
</Tour>
);
}Reset Onboarding
Allow users to restart the onboarding:
function OnboardingResetButton() {
const persistence = usePersistence({ keyPrefix: 'myapp-onboarding' });
const { start } = useTour();
const handleReset = () => {
persistence.reset('onboarding');
start();
};
return (
<button onClick={handleReset}>
Restart Onboarding
</button>
);
}Storage Options
// Default - persists across browser sessions
const persistence = usePersistence({
storage: 'localStorage',
});// Clears when tab is closed
const persistence = usePersistence({
storage: 'sessionStorage',
});Best Practices
Timing: Don't show onboarding immediately. Wait for the app to load and stabilize.
Escape hatch: Always provide a way to skip and a "Don't show again" option.
Mobile: Test your onboarding on mobile. Consider shorter tours or different placements.
Updates: When you update onboarding significantly, consider using a new tour ID so existing users see the new content.
Related
- Basic Tour - Simple tour without persistence
- Headless Custom - Build completely custom UI
- usePersistence Hook - Full API reference