Multi-Page Tours
Build tours that survive SPA navigation and full page reloads with built-in route-aware steps, navigation, and persistence.
A tour can guide users across multiple routes -- pausing on one page, navigating to another, and resuming automatically. react-tourlight has this built in: tag steps with the route they live on, give the provider a navigate callback, and enable persist.
The three pieces
routeon a step -- the path this step's target lives on.navigateon the provider -- how to change routes (your router'spush, orlocation.assign).persiston the provider -- save tour state so it survives a full page reload, and auto-resume on mount.
'use client'
import { useRouter } from 'next/navigation'
import { SpotlightProvider, SpotlightTour } from 'react-tourlight'
import 'react-tourlight/styles.css'
export function Providers({ children }: { children: React.ReactNode }) {
const router = useRouter()
return (
<SpotlightProvider persist navigate={(path) => router.push(path)}>
<SpotlightTour
id="onboarding"
steps={[
{
target: '#dashboard-header',
title: 'Dashboard',
content: 'This is your home base.',
route: '/dashboard',
},
{
// Advancing here calls navigate('/settings'), then waits for the
// target to appear on the destination page.
target: '#profile-section',
title: 'Profile',
content: 'Update your details here.',
route: '/settings',
},
]}
/>
{children}
</SpotlightProvider>
)
}How route works
When the tour advances to a step whose route doesn't match the current window.location.pathname, the provider:
- Calls
navigate(step.route). - Waits (via the built-in
MutationObserver) for the step'stargetto appear on the new page, up towaitForElementTimeout. - Shows the step once the target is found.
For SPA navigation the component stays mounted the whole time. For a full page load (e.g. navigate={(p) => location.assign(p)}), the page unloads and reloads -- persistence takes over (see below).
Route matching
The default matcher compares route against the current pathname and supports:
| Pattern | Matches |
|---|---|
/settings | exactly /settings |
/app/* | /app and anything under it |
/users/:id | /users/42, /users/abc (segment counts must match) |
Need something custom? Override it:
<SpotlightProvider
isRouteActive={(route, pathname) => myMatcher(route, pathname)}
/>Persistence & resume
persist saves tour state on every change and restores it on mount:
// localStorage (default)
<SpotlightProvider persist navigate={navigate} />
// custom storage (sessionStorage, memory, remote, ...)
import { createMemoryStorage } from 'react-tourlight'
<SpotlightProvider persist={createMemoryStorage()} navigate={navigate} />Any object matching the SpotlightStorage shape works -- window.localStorage and window.sessionStorage qualify as-is:
interface SpotlightStorage {
getItem: (key: string) => string | null
setItem: (key: string, value: string) => void
removeItem: (key: string) => void
}Auto-resume
With persist on, a persisted still-active tour is automatically resumed on mount at the exact step it left off (resume defaults to true). This is what makes a full page reload seamless -- the tour reappears on the destination route. To require an explicit start() instead:
<SpotlightProvider persist resume={false} navigate={navigate} />Staleness
Persisted snapshots are discarded when they're no longer safe to restore:
- The tour definition's step count changed (you shipped a new version of the tour).
- The snapshot is older than
persistMaxAge(milliseconds), when set.
<SpotlightProvider persist persistMaxAge={1000 * 60 * 60 * 24} /> // 24hOptions summary
| Prop | Default | Description |
|---|---|---|
persist | undefined (off) | true for localStorage, or a custom SpotlightStorage |
persistKey | 'react-tourlight' | Storage key |
persistMaxAge | undefined (never) | Discard snapshots older than N ms |
resume | true | Auto-resume a persisted active tour on mount |
navigate | undefined | Called with a step's route when it doesn't match |
isRouteActive | built-in matcher | Custom (route, pathname) => boolean |
Persistence is completely opt-in. Without persist, nothing is written to
storage and behavior is identical to previous versions.
Advancing on real navigation
Combine advanceOn (see Interactive Steps) with route steps to advance when the user clicks a real link:
const steps = [
{
target: '#go-to-settings', // a real <a href="/settings"> link
title: 'Open settings',
content: 'Click here to continue.',
interactive: true,
advanceOn: { event: 'click' },
},
{
target: '#profile-section',
title: 'Profile',
content: 'You made it!',
route: '/settings',
},
]The click navigates the app; the tour advances to the next step, which resumes on /settings.
The onBeforeStep escape hatch
For fully custom navigation logic, onBeforeStep runs before a step's target is resolved (and before any route navigation). It may be async and is awaited:
{
target: '#report',
title: 'Your report',
content: 'Generated just now.',
onBeforeStep: async () => {
await openReportsDrawer()
},
}Escape hatch: when predicates (no navigate)
If you'd rather not give react-tourlight a navigate callback, you can still build multi-page tours the manual way: define all steps up front and use when predicates to show only the steps relevant to the current route, navigating yourself via onHide. This predates the route/navigate API and remains fully supported.
import { usePathname, useRouter } from 'next/navigation'
function OnboardingTour() {
const pathname = usePathname()
const router = useRouter()
const steps = [
{
target: '#quick-actions',
title: 'Quick Actions',
content: 'Next, the settings page.',
when: () => pathname === '/dashboard',
onHide: () => {
if (pathname === '/dashboard') router.push('/settings')
},
},
{
target: '#profile-section',
title: 'Profile',
content: 'Update your profile here.',
when: () => pathname === '/settings',
},
]
return <SpotlightTour id="onboarding" steps={steps} />
}Works with any router
react-tourlight never imports a router. Wire navigate (and optionally isRouteActive) to whatever you use:
- Next.js App Router --
const router = useRouter()fromnext/navigation,navigate={(p) => router.push(p)} - Next.js Pages Router --
useRouter()fromnext/router - React Router --
const navigate = useNavigate()fromreact-router-dom - TanStack Router --
const navigate = useNavigate()from@tanstack/react-router - No router --
navigate={(p) => location.assign(p)}(relies onpersist+ auto-resume)
See the Next.js and React Router recipes for complete setups.