react-tourlight

Migrating from React Joyride

Step-by-step guide to migrate from React Joyride to react-tourlight.

If you're coming from React Joyride — especially after hitting React 19 incompatibilities — this guide maps every concept to its react-tourlight equivalent.

Why migrate?

React Joyride uses deprecated React APIs (unmountComponentAtNode, unstable_renderSubtreeIntoContainer) that are removed in React 19. react-tourlight is built from the ground up for modern React with zero deprecated APIs.

Concept mapping

React Joyridereact-tourlightNotes
<Joyride steps={...} /><SpotlightTour id="..." steps={...} />Tours require a unique id
run propuseSpotlight().start(id)Imperative control via hook
continuous propAlways continuousStep-by-step is the default
callback with STATUSonComplete / onSkip propsCleaner event model
styles proptheme prop on ProviderCentralized theming
floaterPropsFloating UI peer depUses @floating-ui/react-dom
disableOverlayoverlayColor="transparent"Set overlay to transparent
spotlightPaddingspotlightPadding on stepSame concept, per-step
localelabels prop on Provideri18n for button text
tooltipComponentrenderTooltip render propFull control via render props

Step-by-step migration

1. Install react-tourlight

npm uninstall react-joyride
npm install react-tourlight @floating-ui/react-dom

2. Replace the provider

Before (Joyride):

import Joyride from 'react-joyride'

function App() {
  const [run, setRun] = useState(false)
  return (
    <>
      <Joyride
        steps={steps}
        run={run}
        continuous
        callback={(data) => {
          if (data.status === 'finished') handleComplete()
        }}
      />
      <button onClick={() => setRun(true)}>Start Tour</button>
    </>
  )
}

After (react-tourlight):

import { SpotlightProvider, SpotlightTour, useSpotlight } from 'react-tourlight'
import 'react-tourlight/styles.css'

function App() {
  return (
    <SpotlightProvider>
      <SpotlightTour
        id="onboarding"
        steps={steps}
        onComplete={() => handleComplete()}
      />
      <StartButton />
    </SpotlightProvider>
  )
}

function StartButton() {
  const { start } = useSpotlight()
  return <button onClick={() => start('onboarding')}>Start Tour</button>
}

3. Convert step format

Before:

const steps = [
  {
    target: '.my-element',
    content: 'This is the first step',
    title: 'Step 1',
    placement: 'bottom',
    disableBeacon: true,
  },
]

After:

const steps = [
  {
    target: '.my-element',
    content: 'This is the first step',
    title: 'Step 1',
    placement: 'bottom',
    // No beacon concept — spotlights start directly
  },
]

Key differences:

  • No disableBeacon — react-tourlight doesn't have beacons
  • content accepts ReactNode, not just strings
  • placement supports 'auto' for smart positioning
  • Add spotlightPadding and spotlightRadius per step

4. Update event handling

Before:

callback={(data) => {
  const { status, type, index } = data
  if (status === 'finished') { /* done */ }
  if (status === 'skipped') { /* skipped */ }
  if (type === 'step:after') { /* step changed */ }
}}

After:

<SpotlightTour
  id="onboarding"
  steps={steps}
  onComplete={() => { /* done */ }}
  onSkip={(stepIndex) => { /* skipped at step */ }}
/>

5. Update custom tooltips

Before:

tooltipComponent={({ step, primaryProps, backProps, skipProps, index, size }) => (
  <div>
    <h3>{step.title}</h3>
    <p>{step.content}</p>
    <button {...backProps}>Back</button>
    <button {...primaryProps}>Next</button>
  </div>
)}

After:

renderTooltip={({ step, next, previous, skip, currentIndex, totalSteps }) => (
  <div>
    <h3>{step.title}</h3>
    <p>{step.content}</p>
    <button onClick={previous} disabled={currentIndex === 0}>Back</button>
    <button onClick={next}>
      {currentIndex < totalSteps - 1 ? 'Next' : 'Done'}
    </button>
  </div>
)}

What you gain

After migrating, you get:

  • React 19 compatibility — no deprecated APIs
  • Better accessibility — focus trap, full keyboard nav, ARIA roles, screen reader support
  • Smaller bundle — ~5KB vs ~30KB
  • CSS clip-path overlay — works perfectly in dark mode (no mix-blend-mode hacks)
  • Async element supportMutationObserver-based waiting for lazy-loaded content