Build a complete Next.js 15 onboarding flow: tour + checklist + analytics
A product tour by itself is an introduction — not an onboarding flow. A real onboarding flow guides users from sign-up to activation across multiple sessions, tracks which steps they've completed, and reports what's working to an analytics pipeline. This tutorial builds that full flow in a Next.js 15 App Router project: a three-step first-run tour, a persistent activation checklist that survives reloads, and an analytics plugin that reports every event to your provider of choice. Ship in about forty-five minutes; no Server Component fighting, no SSR workarounds, no third-party runtime.
pnpm add @tour-kit/react @tour-kit/checklists @tour-kit/analyticsWhat you'll build
- A first-run
Tourthat auto-starts on a new user's first visit to/dashboard. - A persistent
Checklistwidget with four activation tasks, progress persistence, and task dependencies. - An analytics integration that reports
tour_started,step_viewed,tour_completed,checklist_task_completed, and more to PostHog (swap for Mixpanel / Amplitude / GA4 by changing one line).
The finished flow looks like what Linear, Vercel, or Stripe ship on first log-in — minus the third-party runtime and the per-seat pricing.
Prerequisites
- Next.js 15 project using the App Router (
app/directory). - React 18 or 19.
- A PostHog project (free tier works) or another analytics provider you already use.
If you don't have a Next.js project yet:
pnpm create next-app@latest onboarding-demo
cd onboarding-demoStep 1 — Install the packages
pnpm add @tour-kit/react @tour-kit/checklists @tour-kit/analytics posthog-jsThe three Tour Kit packages have a combined gzipped footprint well under 20KB — @tour-kit/analytics adds only the plugin wiring; the provider SDK (posthog-js here) is what you'd ship anyway.
Step 2 — Create the onboarding provider
Tour Kit's onboarding pieces all plug into a shared provider tree. Put them in a dedicated client component so Server Components above can stay untouched.
'use client'
import { TourProvider } from '@tour-kit/react'
import { ChecklistProvider } from '@tour-kit/checklists'
import { AnalyticsProvider, PostHogPlugin } from '@tour-kit/analytics'
import posthog from 'posthog-js'
import { useEffect } from 'react'
export function OnboardingProvider({ children }: { children: React.ReactNode }) {
useEffect(() => {
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
})
}, [])
return (
<AnalyticsProvider plugins={[new PostHogPlugin(posthog)]}>
<TourProvider>
<ChecklistProvider persistence="localStorage">
{children}
</ChecklistProvider>
</TourProvider>
</AnalyticsProvider>
)
}Add this provider to app/layout.tsx:
import { OnboardingProvider } from './_providers/onboarding-provider'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<OnboardingProvider>{children}</OnboardingProvider>
</body>
</html>
)
}Server Components above OnboardingProvider are untouched — this is the App Router pattern for mixing client-side state with a predominantly server-rendered app.
Step 3 — Build the first-run tour
Add three steps to the dashboard, starting the tour automatically for new users:
'use client'
import { Tour, TourStep, TourCard, TourOverlay, useTour } from '@tour-kit/react'
import { useEffect } from 'react'
function AutoStart() {
const { start, isComplete } = useTour('dashboard-onboarding')
useEffect(() => {
if (!isComplete) start()
}, [isComplete, start])
return null
}
export default function Dashboard() {
return (
<Tour id="dashboard-onboarding" persistence="localStorage">
<TourStep
target="#create-project"
title="Create your first project"
content="Everything lives inside a project. Start by creating one."
placement="bottom"
/>
<TourStep
target="#invite-team"
title="Invite your teammates"
content="Onboarding is faster with a partner in crime."
placement="right"
/>
<TourStep
target="#install-cli"
title="Install the CLI"
content="The CLI is how you ship. We'll walk through it next."
placement="left"
/>
<TourOverlay />
<TourCard />
<AutoStart />
<main>
<h1>Dashboard</h1>
<button id="create-project">Create project</button>
<button id="invite-team">Invite team</button>
<button id="install-cli">Install CLI</button>
</main>
</Tour>
)
}persistence="localStorage" means the tour won't restart after the user completes it. For testing, clear localStorage to reset.
Step 4 — Add the activation checklist
The tour introduces the workflow. The checklist sticks around until the user actually completes it — including across sessions.
'use client'
import { Checklist, ChecklistTask, ChecklistProgress } from '@tour-kit/checklists'
export function ActivationChecklist() {
return (
<Checklist id="activation" title="Get started in 4 steps">
<ChecklistProgress />
<ChecklistTask
id="create-project"
title="Create your first project"
description="Every workspace starts with a project."
/>
<ChecklistTask
id="invite-team"
title="Invite a teammate"
description="Onboarding is easier together."
dependsOn="create-project"
/>
<ChecklistTask
id="install-cli"
title="Install the CLI"
description="Ship from your terminal."
/>
<ChecklistTask
id="first-deploy"
title="Ship your first deploy"
description="The activation event — congrats."
dependsOn="install-cli"
/>
</Checklist>
)
}Add the widget to the dashboard:
import { ActivationChecklist } from './_components/activation-checklist'
// Inside the Tour, alongside your dashboard UI:
<ActivationChecklist />Task dependencies (dependsOn) lock later steps until prerequisites complete. "Invite a teammate" stays disabled until "Create your first project" is done. The checklist persists via ChecklistProvider's localStorage adapter — close the browser, come back, the progress is still there.
Step 5 — Mark tasks complete from server actions
When a user creates a project via a server action, flag the corresponding task:
'use client'
import { useChecklist } from '@tour-kit/checklists'
import { createProject } from '../actions'
export function NewProjectForm() {
const { complete } = useChecklist('activation')
async function handleSubmit(formData: FormData) {
const result = await createProject(formData)
if (result.ok) {
complete('create-project')
}
}
return (
<form action={handleSubmit}>
<input name="name" placeholder="Project name" />
<button type="submit">Create</button>
</form>
)
}The Server Action (createProject) stays in a Server Component file (actions.ts). The checklist update happens in the Client Component on success — exactly how Next.js App Router wants you to split server and client logic.
Step 6 — Verify analytics events fire
Open your PostHog dashboard. Start the tour as a brand-new user. You should see:
tour_started { tour_id: "dashboard-onboarding" }
tour_step_viewed { tour_id: "dashboard-onboarding", step_index: 0 }
tour_step_viewed { tour_id: "dashboard-onboarding", step_index: 1 }
tour_step_viewed { tour_id: "dashboard-onboarding", step_index: 2 }
tour_completed { tour_id: "dashboard-onboarding" }
checklist_task_completed { checklist_id: "activation", task_id: "create-project" }The plugin fires events automatically — no manual track() calls needed. To swap provider, change the plugin in OnboardingProvider:
// Mixpanel
import { MixpanelPlugin } from '@tour-kit/analytics'
new MixpanelPlugin(mixpanel)
// Amplitude
import { AmplitudePlugin } from '@tour-kit/analytics'
new AmplitudePlugin(amplitude)
// GA4
import { GA4Plugin } from '@tour-kit/analytics'
new GA4Plugin({ measurementId: 'G-XXXXX' })
// Custom — implement the plugin interface
import { AnalyticsPlugin } from '@tour-kit/analytics'
class MySink implements AnalyticsPlugin {
track(event: string, props: Record<string, unknown>) {
fetch('/api/track', { method: 'POST', body: JSON.stringify({ event, props }) })
}
}Step 7 — Measure activation, not just engagement
Now that events are flowing, build a PostHog funnel:
user_signed_uptour_startedtour_completedchecklist_task_completed(withtask_id == "first-deploy")
That's your activation funnel. It tells you where users drop off — and because the tour and checklist share IDs across the same user, you can segment by completion state in future queries.
FAQ
Do I need the Pro packages (@tour-kit/checklists, @tour-kit/analytics)?
The tour (@tour-kit/react) and hints are free under MIT. Checklists, analytics, announcements, adoption, media, and scheduling are Pro — $99 one-time for all of them. For production onboarding flows, the Pro packages pay for themselves in the first week.
Does this work with Next.js Server Components?
Yes. OnboardingProvider is a Client Component and lives under <body> in app/layout.tsx. Everything above it in the tree (including server-rendered layouts and pages) stays server-rendered. Only the interactive onboarding pieces need to be client-side.
Can I gate the tour on a user segment (e.g., only paying users)?
Yes. Put a condition around AutoStart:
{user.plan !== 'free' && user.createdAt > firstRunCutoff && <AutoStart />}Or gate at the TourProvider level if you want to hide tour UI entirely.
What if my users span multiple timezones for scheduled onboarding?
Pair this flow with @tour-kit/scheduling (Pro) to time-box announcements or checklists to a specific timezone or business-hours window. See the scheduling docs for IANA timezone support.
What about mobile?
All Tour Kit components are responsive by default, but mobile-first onboarding often uses different UX (bottom sheets, simpler tours). The headless packages (@tour-kit/react/headless) give you complete markup control to implement mobile-specific variants without forking the library.
Next steps
@tour-kit/checklistsreference — task dependencies, custom completion checks.@tour-kit/analyticsreference — plugin interface and built-in providers.- SaaS onboarding use case — extending this pattern to a real dashboard.
Ready to build? Install the Tour Kit packages and ship your first complete onboarding flow today.
Related articles
Build an accessible product tour in React: a step-by-step tutorial
Build a WCAG 2.1 AA accessible product tour in React step by step. Focus trapping, keyboard navigation, screen reader support, and prefers-reduced-motion in under 30 minutes.
Read article
How to Add a Product Tour to a React 19 App in 5 Minutes
Add a working product tour to your React 19 app with userTourKit. Covers useTransition async steps, ref-as-prop targeting, and full TypeScript examples.
Read article
How to Add a Product Tour to a Next.js App Router Project
Step-by-step guide to integrating userTourKit into a Next.js 15 App Router project. Covers Server Components, client boundaries, multi-page routing, and TypeScript setup.
Read article
Migrating from Driver.js to Tour Kit: Adding Headless Power
Step-by-step guide to replacing Driver.js with userTourKit in a React project. Covers step definitions, popover rendering, highlight migration, callbacks, and multi-page tours.
Read article