Tracking Tour Completion
Track tour completion, skip events, and step-level drop-off with analytics providers.
Understanding how users interact with your tours helps you improve onboarding flows. react-tourlight provides callbacks at both the provider and tour level that integrate with any analytics service.
Basic tracking with callbacks
Use onComplete and onSkip on <SpotlightProvider> to track all tours globally, or on <SpotlightTour> for specific tours:
<SpotlightProvider
onComplete={(tourId) => {
console.log(`Tour "${tourId}" completed`)
}}
onSkip={(tourId, stepIndex) => {
console.log(`Tour "${tourId}" skipped at step ${stepIndex}`)
}}
>
{/* ... */}
</SpotlightProvider>Tracking with onStateChange
For detailed step-level tracking, use onStateChange. It fires on every state transition -- start, step change, complete, and skip:
import type { TourState } from 'react-tourlight'
<SpotlightProvider
onStateChange={(tourId: string, state: TourState) => {
// state.status is 'idle' | 'active' | 'completed'
// state.currentStepIndex tells you which step the user is on
// state.seenSteps tracks which steps were viewed
// state.completedAt / state.skippedAt provide timestamps
if (state.status === 'active') {
analytics.track('tour_step_viewed', {
tourId,
stepIndex: state.currentStepIndex,
seenSteps: state.seenSteps.length,
})
}
}}
>localStorage persistence
Track whether users have completed tours so you don't show them again:
import { useState } from 'react'
import { SpotlightProvider, SpotlightTour, useSpotlight } from 'react-tourlight'
import type { TourState } from 'react-tourlight'
function App() {
const [tourState, setTourState] = useState<Record<string, TourState>>(() => {
try {
const stored = localStorage.getItem('tour-state')
return stored ? JSON.parse(stored) : {}
} catch {
return {}
}
})
const handleStateChange = (tourId: string, state: TourState) => {
setTourState((prev) => {
const next = { ...prev, [tourId]: state }
localStorage.setItem('tour-state', JSON.stringify(next))
return next
})
}
// Don't start the tour if already completed
const isCompleted = tourState['onboarding']?.status === 'completed'
return (
<SpotlightProvider
initialState={tourState}
onStateChange={handleStateChange}
>
<SpotlightTour id="onboarding" steps={steps} />
{!isCompleted && <AutoStartTour />}
</SpotlightProvider>
)
}
function AutoStartTour() {
const { start } = useSpotlight()
// Start the tour on mount
useEffect(() => {
start('onboarding')
}, [start])
return null
}PostHog
import posthog from 'posthog-js'
<SpotlightProvider
onComplete={(tourId) => {
posthog.capture('tour_completed', { tour_id: tourId })
}}
onSkip={(tourId, stepIndex) => {
posthog.capture('tour_skipped', {
tour_id: tourId,
skipped_at_step: stepIndex,
})
}}
onStateChange={(tourId, state) => {
if (state.status === 'active') {
posthog.capture('tour_step_viewed', {
tour_id: tourId,
step_index: state.currentStepIndex,
})
}
}}
>Amplitude
import * as amplitude from '@amplitude/analytics-browser'
<SpotlightProvider
onComplete={(tourId) => {
amplitude.track('Tour Completed', { tourId })
}}
onSkip={(tourId, stepIndex) => {
amplitude.track('Tour Skipped', { tourId, stepIndex })
}}
onStateChange={(tourId, state) => {
if (state.status === 'active') {
amplitude.track('Tour Step Viewed', {
tourId,
stepIndex: state.currentStepIndex,
})
}
}}
>Mixpanel
import mixpanel from 'mixpanel-browser'
<SpotlightProvider
onComplete={(tourId) => {
mixpanel.track('Tour Completed', { tour_id: tourId })
}}
onSkip={(tourId, stepIndex) => {
mixpanel.track('Tour Skipped', {
tour_id: tourId,
skipped_at_step: stepIndex,
})
}}
onStateChange={(tourId, state) => {
if (state.status === 'active') {
mixpanel.track('Tour Step Viewed', {
tour_id: tourId,
step_index: state.currentStepIndex,
})
}
}}
>Tracking step-level drop-off
To understand where users abandon tours, compare seenSteps against total steps in the onSkip callback:
<SpotlightTour
id="onboarding"
steps={steps}
onSkip={(stepIndex) => {
analytics.track('tour_abandoned', {
tourId: 'onboarding',
abandonedAtStep: stepIndex,
totalSteps: steps.length,
completionRate: stepIndex / steps.length,
// Which steps did they see before dropping off?
stepsViewed: stepIndex + 1,
})
}}
onComplete={() => {
analytics.track('tour_completed', {
tourId: 'onboarding',
totalSteps: steps.length,
})
}}
/>This data helps you identify steps that are confusing, too long, or not valuable to users -- so you can iterate on your onboarding flow.