Migrating from React Joyride
Step-by-step guide to migrate from React Joyride to react-tourlight.
If you're coming from React Joyride, this guide maps every concept to its react-tourlight equivalent.
Why migrate?
React Joyride is a solid, MIT-licensed library and, as of v3.2, works on React 19. People move to react-tourlight for what Joyride doesn't do:
- Headless core — build your own tooltip and overlay on
react-tourlight/corewith no CSS and no Floating UI. - Multi-page tours — steps with a
route, persisted across navigation and full reloads, resumed automatically. - True interactive steps — the spotlight hole is a real gap in the pointer surface, so typing, hovering, dragging, and scrolling reach the target.
advanceOncontinues the tour on a real click. - Accessibility — focus trap,
inerton the rest of the page, live-region announcements, and focus restoration. - Dark-mode-safe overlay — a CSS
clip-pathcutout instead ofmix-blend-mode. - Server Components — ships its own
"use client"directive, so you can import it straight intoapp/layout.tsx. - Smaller — ~8 kB gzipped headless, ~19 kB styled.
Concept mapping
| React Joyride | react-tourlight | Notes |
|---|---|---|
<Joyride steps={...} /> | <SpotlightTour id="..." steps={...} /> | Tours require a unique id |
run prop | useSpotlight().start(id) | Imperative control via hook |
continuous prop | Always continuous | Step-by-step is the default |
callback with STATUS | onComplete / onSkip props | Cleaner event model |
styles prop | theme prop on Provider | Centralized theming |
floaterProps | Floating UI peer dep | Uses @floating-ui/react-dom |
disableOverlay | overlayColor="transparent" | Set overlay to transparent |
spotlightPadding | spotlightPadding on step | Same concept, per-step |
locale | labels prop on Provider | i18n for button text |
tooltipComponent | renderTooltip render prop | Full control via render props |
Step-by-step migration
1. Install react-tourlight
npm uninstall react-joyride
npm install react-tourlight @floating-ui/react-dom2. 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',
// Tours start directly; opt-in beacons are a separate component (see below)
},
]Key differences:
- No
disableBeacon— tours start on the first step directly. If you want Joyride-style opt-in hotspots, render<SpotlightBeacon target="..." tour="..." />next to the element instead targetalso accepts a React ref or a resolver function (() => element)contentacceptsReactNode, not just stringsplacementsupports'auto'for smart positioning- Add
spotlightPaddingandspotlightRadiusper 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}
onStart={() => { /* started */ }}
onStepChange={(stepIndex, step) => { /* step changed */ }}
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, close, isFirst, isLast }) => (
<div>
<button onClick={close} aria-label="Close">×</button>
<h3>{step.title}</h3>
<p>{step.content}</p>
<button onClick={previous} disabled={isFirst}>Back</button>
<button onClick={next}>{isLast ? 'Done' : 'Next'}</button>
</div>
)}6. Replace beacons (optional)
Joyride shows a pulsing beacon before each step unless disableBeacon is set. react-tourlight separates the two ideas: tours start directly, and <SpotlightBeacon> is an explicit, opt-in hotspot you place where you want one:
import { SpotlightBeacon } from 'react-tourlight'
<SpotlightBeacon target="#export-button" tour="onboarding" stepIndex={2} />What you gain
After migrating, you get:
- Headless core — build a fully custom UI on the same engine
- Multi-page tours — persist and resume across routes and reloads
- True interactive steps — real event pass-through and
advanceOn - Better accessibility — focus trap,
inert, full keyboard nav, ARIA roles, screen reader support - CSS clip-path overlay — works in dark mode (no
mix-blend-modehacks) - Async element support —
MutationObserver-based waiting for lazy-loaded content, for selectors, refs, and resolver functions