Multi-Step Tours
Build guided walkthroughs with multi-step tours, lifecycle hooks, conditional steps, and interactive elements.
Defining a tour
A tour is a sequence of steps registered with <SpotlightTour>. Each step highlights a target element and displays a tooltip with a title and content.
import { SpotlightTour } from 'react-tourlight'
const steps = [
{
target: '#dashboard-header',
title: 'Welcome',
content: 'This is your dashboard. Let us show you around.',
placement: 'bottom',
},
{
target: '#analytics-panel',
title: 'Analytics',
content: 'Track your key metrics in real time.',
placement: 'right',
},
{
target: '#settings-button',
title: 'Settings',
content: 'Customize your workspace from here.',
placement: 'left',
},
]
function App() {
return (
<SpotlightTour
id="dashboard-tour"
steps={steps}
onComplete={() => console.log('Done!')}
onSkip={(stepIndex) => console.log(`Skipped at step ${stepIndex}`)}
/>
)
}<SpotlightTour> doesn't render any UI itself. It registers the steps with the nearest <SpotlightProvider>, making them available to start via useSpotlight().start('dashboard-tour').
Step configuration
Every step requires target, title, and content. Everything else is optional.
const step: SpotlightStep = {
// Required
target: '#my-element', // CSS selector or React ref
title: 'Feature Name',
content: 'Description of the feature.',
// Positioning
placement: 'bottom', // 'top' | 'bottom' | 'left' | 'right' | 'auto'
spotlightPadding: 8, // padding around the cutout (px)
spotlightRadius: 8, // border radius of the cutout (px)
// Interactivity
interactive: false, // let users click the highlighted element
disableOverlayClose: false, // prevent overlay click from dismissing
// CTA button
action: {
label: 'Try it now',
onClick: () => openFeature(),
},
// Conditional display
when: () => userHasPermission(), // skip step if returns false
// Lifecycle hooks
onBeforeShow: () => loadData(), // called before step is shown (can be async)
onAfterShow: () => trackView(), // called after step is visible
onHide: () => cleanup(), // called when step is hidden
}Using React refs as targets
Instead of CSS selectors, you can use refs. This is useful when elements don't have stable IDs or data- attributes:
import { useSpotlightTarget, SpotlightTour } from 'react-tourlight'
function Dashboard() {
const searchRef = useSpotlightTarget()
const navRef = useSpotlightTarget()
return (
<>
<SpotlightTour
id="onboarding"
steps={[
{ target: searchRef, title: 'Search', content: 'Find anything.' },
{ target: navRef, title: 'Navigation', content: 'Browse projects.' },
]}
/>
<input ref={searchRef} placeholder="Search..." />
<nav ref={navRef}>{/* ... */}</nav>
</>
)
}Starting and stopping tours
Use the useSpotlight hook to control tours programmatically:
import { useSpotlight } from 'react-tourlight'
function TourControls() {
const { start, stop, next, previous, skip, isActive, currentStep, totalSteps } = useSpotlight()
return (
<div>
{!isActive ? (
<button onClick={() => start('onboarding')}>Start Tour</button>
) : (
<>
<p>Step {currentStep + 1} of {totalSteps}</p>
<button onClick={previous}>Previous</button>
<button onClick={next}>Next</button>
<button onClick={skip}>Skip</button>
<button onClick={stop}>Stop</button>
</>
)}
</div>
)
}You can also jump to a specific step by index:
const { goToStep } = useSpotlight()
goToStep(2) // jump to the third stepTour lifecycle callbacks
Handle tour completion and skip events at both the provider and tour level:
// Provider-level — fires for any tour
<SpotlightProvider
onComplete={(tourId) => {
console.log(`Tour "${tourId}" completed`)
}}
onSkip={(tourId, stepIndex) => {
console.log(`Tour "${tourId}" skipped at step ${stepIndex}`)
}}
>
{/* ... */}
</SpotlightProvider>
// Tour-level — fires for this specific tour
<SpotlightTour
id="onboarding"
steps={steps}
onComplete={() => markOnboardingDone()}
onSkip={(stepIndex) => trackDropoff(stepIndex)}
/>Conditional steps
Use the when predicate to conditionally include or skip steps. The step is skipped if when returns false. This can be synchronous or asynchronous:
const steps: SpotlightStep[] = [
{
target: '#basic-feature',
title: 'Basic Feature',
content: 'Available to everyone.',
},
{
target: '#admin-panel',
title: 'Admin Panel',
content: 'Manage users and permissions.',
when: () => currentUser.role === 'admin',
},
{
target: '#beta-feature',
title: 'Beta Feature',
content: 'Try out our latest experiment.',
when: async () => {
const flags = await fetchFeatureFlags()
return flags.betaEnabled
},
},
]Step lifecycle hooks
Each step can define hooks that run at specific points in its lifecycle:
{
target: '#data-table',
title: 'Data Table',
content: 'Your latest data, refreshed.',
// Runs before the step is shown. Can be async — the step
// waits for the promise to resolve before appearing.
onBeforeShow: async () => {
await fetchLatestData()
},
// Runs after the step tooltip is visible on screen.
onAfterShow: () => {
analytics.track('tour_step_viewed', { step: 'data-table' })
},
// Runs when the user leaves this step (next, previous, skip, or close).
onHide: () => {
resetTableFilters()
},
}Interactive steps
By default, the overlay prevents interaction with background elements. Set interactive: true to allow users to click and interact with the highlighted element:
{
target: '#theme-toggle',
title: 'Try Dark Mode',
content: 'Click the toggle to switch themes.',
interactive: true,
}This is useful for steps that ask users to perform an action (toggle a switch, click a button, type in an input) as part of the tour.
Action buttons
Add a custom CTA button inside the tooltip with the action property:
{
target: '#invite-button',
title: 'Invite Your Team',
content: 'Collaboration works best with teammates.',
action: {
label: 'Invite Now',
onClick: () => openInviteModal(),
},
}The action button appears alongside the default navigation buttons (Next, Previous, Skip).