Next.js Integration
Set up react-tourlight with Next.js App Router, including client component wrappers and SSR considerations.
react-tourlight is a client-side library that uses browser APIs (MutationObserver, document.querySelector, inert).
As of v0.2.0, react-tourlight's published entry point ships its own "use client"
directive. That means SpotlightProvider and SpotlightTour can be imported
directly into a Server Component file (like app/layout.tsx) and rendered
there without you writing a wrapper — Next.js reads the client boundary from
inside the package itself. The wrapper pattern below is still useful if you
want a single place to configure the provider (theme, labels, callbacks) or
to keep app/layout.tsx free of react-tourlight imports, but it's no longer
required just to satisfy the Server/Client Component boundary.
App Router setup
1. (Optional) Create a client wrapper
Create a SpotlightWrapper component marked with 'use client':
'use client'
import { SpotlightProvider } from 'react-tourlight'
import 'react-tourlight/styles.css'
export function SpotlightWrapper({ children }: { children: React.ReactNode }) {
return (
<SpotlightProvider theme="auto">
{children}
</SpotlightProvider>
)
}2. Add to your root layout
Import the wrapper in your root layout. The layout itself can remain a Server Component:
import { SpotlightWrapper } from '@/components/spotlight-wrapper'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<SpotlightWrapper>
{children}
</SpotlightWrapper>
</body>
</html>
)
}3. Define tours in client components
Tour definitions and the useSpotlight hook must be used in Client Components:
'use client'
import { SpotlightTour, useSpotlight } from 'react-tourlight'
export function OnboardingTour() {
return (
<SpotlightTour
id="onboarding"
steps={[
{
target: '#search',
title: 'Search',
content: 'Find anything instantly.',
placement: 'bottom',
},
{
target: '#sidebar',
title: 'Navigation',
content: 'Browse your projects.',
placement: 'right',
},
]}
onComplete={() => {
localStorage.setItem('onboarding-done', 'true')
}}
/>
)
}
export function StartTourButton() {
const { start } = useSpotlight()
return (
<button onClick={() => start('onboarding')}>
Take the tour
</button>
)
}SSR considerations
react-tourlight renders nothing on the server. The overlay, tooltip, and spotlight cutout are all client-side only. This means:
- No hydration mismatch issues -- the spotlight UI is only rendered in the browser
- Server Components can render target elements (
#search,#sidebar, etc.) normally - Tour state and
useSpotlightare only available in Client Components
Dynamic import pattern
If you want to code-split the tour logic out of your initial bundle, use next/dynamic:
'use client'
import dynamic from 'next/dynamic'
const OnboardingTour = dynamic(
() => import('./onboarding-tour').then((mod) => ({ default: mod.OnboardingTour })),
{ ssr: false }
)
export function LazyOnboardingTour() {
return <OnboardingTour />
}This keeps the tour code out of the initial JavaScript bundle. The ssr: false option ensures it only loads on the client.
Multi-page tours with App Router
For tours that span multiple routes, wire router.push to the provider's navigate prop, enable persist, and tag each step with the route it lives on. Put this in your client wrapper so the whole app is covered:
'use client'
import { useRouter } from 'next/navigation'
import { SpotlightProvider, SpotlightTour } from 'react-tourlight'
import 'react-tourlight/styles.css'
export function SpotlightWrapper({ children }: { children: React.ReactNode }) {
const router = useRouter()
return (
<SpotlightProvider theme="auto" persist navigate={(path) => router.push(path)}>
<SpotlightTour
id="onboarding"
steps={[
{
target: '#dashboard-nav',
title: 'Dashboard',
content: 'Your home base.',
route: '/dashboard',
},
{
target: '#settings-panel',
title: 'Settings',
content: 'Configure your workspace.',
route: '/settings',
},
]}
/>
{children}
</SpotlightProvider>
)
}When the tour advances to the settings step while on /dashboard, react-tourlight calls router.push('/settings') and waits for #settings-panel before showing the step. With persist, a full reload mid-tour resumes automatically.
Dynamic segments work too -- route: '/projects/:id' matches /projects/42.
Alternative: when predicates (no navigate)
If you'd rather navigate yourself, you can still use usePathname() with when predicates and onHide:
'use client'
import { usePathname } from 'next/navigation'
import { SpotlightTour } from 'react-tourlight'
export function OnboardingTour() {
const pathname = usePathname()
return (
<SpotlightTour
id="onboarding"
steps={[
{
target: '#dashboard-nav',
title: 'Dashboard',
content: 'Your home base.',
when: () => pathname === '/',
},
{
target: '#settings-panel',
title: 'Settings',
content: 'Configure your workspace.',
when: () => pathname === '/settings',
},
]}
/>
)
}See the Multi-Page Tours guide for the complete route + persistence reference.