How It Works
Joyride runs as a state machine with two dimensions: tour status and step lifecycle.
The tour can also be paused or skipped from running — see transitions below.
While the tour is running, each step progresses through the lifecycle below.
Both beacon and tooltip phases have a preceding _before phase — see full lifecycle below.
Tour Status
The tour moves through these statuses:
| Status | Meaning |
|---|---|
idle | Initial state. No tour running. |
waiting | Tour started but no steps are loaded yet. Transitions to running once steps are available. |
ready | Steps are loaded, tour is ready to start. |
running | Tour is active and displaying steps. |
paused | Tour was stopped mid-way via controls.stop(). Can be resumed. |
finished | All steps completed. |
skipped | User clicked Skip to exit early. |
Transitions
Starting
run={true}orcontrols.start()→running(orwaitingif no steps loaded yet)controls.start()frompaused→running(resumes tour)waiting→runningautomatically when steps become available
Stopping
run={false}orcontrols.stop()→paused- Skip button or
controls.skip()→skipped - Last step completed or Close button on final step →
finished
Resetting
controls.reset()→readycontrols.reset(true)→running(restarts the tour)
The run prop is the declarative way to start and stop the tour.
Setting run={true} calls controls.start() internally, and run={false} calls controls.stop().
Both work with the component and the hook.
See STATUS for the constant values.
Step Lifecycle
Each step goes through these phases:
| Phase | What happens | When |
|---|---|---|
init | Target lookup. before hook runs if defined. | Always |
ready | Target found and visible. | Always |
beacon_before | Scroll to target. Positioning calculated. | skipBeacon is false |
beacon | Beacon displayed. Waiting for user click. | Beacon visible |
tooltip_before | Scroll to target. Positioning calculated. | Always |
tooltip | Tooltip displayed and interactive. | Always |
complete | after hook fires, tour advances. | Always |
The beacon_before and beacon phases are skipped when skipBeacon is set, placement is center, or in continuous tours navigating with Next/Prev.
See LIFECYCLE for the constant values.
Event Sequence
As a step progresses through its lifecycle, Joyride fires events via the onEvent callback. Here's the typical sequence:
tour:start
→ step:before_hook (only if step has a `before` hook)
→ step:before
→ scroll:start (only if scrolling is needed)
→ scroll:end
→ beacon (skipped in continuous mode with Next/Prev)
→ tooltip
→ step:after
→ step:after_hook (only if step has an `after` hook)
→ ... next step ...
tour:endError events (error:target_not_found, error) can fire at any point if the target is missing or a hook fails.
See the Events page for how to handle these in your code.
Controlled vs Uncontrolled
Uncontrolled (default)
Joyride manages the step index internally. The built-in buttons (Next, Back, Skip, Close) handle navigation. You can also use controls for programmatic navigation.
This is the default for nearly all cases. Async before and after step hooks handle conditional UI, route transitions, and animated elements without giving up internal index management.
<Joyride
continuous
run={true}
steps={steps}
onEvent={(data, controls) => {
// React to events, but Joyride handles navigation
}}
/>Controlled
Reach for controlled mode only when an external system owns the index — for example, a persistence layer that resumes the tour at a specific step on reload, or a parent orchestrator coordinating multiple tours from one state tree.
For conditional UI (open a sidebar, switch a section, mount a modal), use a before step hook instead. Driving stepIndex from a useEffect that reacts to app state desynchronizes the lifecycle and breaks overlay/keyboard handlers.
Even when controlled mode is justified, you'll still rely on before hooks for timing — the Controlled Tour demo uses them on every step. Controlled mode adds work on top of async hooks; it does not replace them.
Set stepIndex to take full control. You manage the index yourself via onEvent:
const [stepIndex, setStepIndex] = useState(0);
const [run, setRun] = useState(false);
<Joyride
continuous
run={run}
stepIndex={stepIndex}
steps={steps}
onEvent={(data, controls) => {
const { action, index, status, type } = data;
if ([EVENTS.STEP_AFTER, EVENTS.TARGET_NOT_FOUND].includes(type)) {
setStepIndex(index + (action === ACTIONS.PREV ? -1 : 1));
} else if ([STATUS.FINISHED, STATUS.SKIPPED].includes(status)) {
setRun(false);
}
}}
/>The useJoyride hook provides a controls object for programmatic navigation in uncontrolled mode.
The Controlled Tour demo shows a full implementation with multi-step state management.