react-tourlight

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':

components/spotlight-wrapper.tsx
'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:

app/layout.tsx
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:

components/onboarding-tour.tsx
'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 useSpotlight are 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:

components/lazy-tour.tsx
'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:

components/spotlight-wrapper.tsx
'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:

components/onboarding-tour.tsx
'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.