Technical Documentation: gilbarbara/react-joyride
โน๏ธ Provenance: Multi-Tier Ingestion ยท CodeWiki Reference
Overview (README.md)
React Joyride
Create awesome tours for your app!
Showcase your app to new users or explain the functionality of new features.
Highlights
- ๐จ Easy to use: Just set the
stepsand you're good to go - ๐ง Customizable: Use your own components and styles
- โฟ Accessible: Focus trapping, keyboard navigation, and ARIA support
- ๐ฆ Lightweight: ~30% smaller bundle than v2
- ๐ Broad compatibility: React 16.8+ through React 19
- ๐ฅ๏ธ SSR-safe: Works with Next.js, Remix, and other server-rendering frameworks
Resources
Setup
npm i react-joyrideQuick Start
Component API
import { Joyride } from 'react-joyride';
const steps = [
{ target: '.my-first-step', content: 'This is my awesome feature!' },
{ target: '.my-other-step', content: 'This is another awesome feature!' },
];
export function App() {
return (
<div>
<Joyride run steps={steps} />
{/* your app */}
</div>
);
}Hook API
import { useJoyride } from 'react-joyride';
const steps = [
{ target: '.my-first-step', content: 'This is my awesome feature!' },
{ target: '.my-other-step', content: 'This is another awesome feature!' },
];
export function App() {
const { controls, state, Tour } = useJoyride({ steps });
return (
<div>
<button onClick={() => controls.start()}>Start Tour</button>
{Tour}
{/* your app */}
</div>
);
}Agent Skill
Enable AI assistants to implement guided tours with React Joyride.
The skill gives your AI assistant comprehensive knowledge of the API, step configuration, events, custom components, and common patterns.
npx skills add gilbarbara/react-joyrideMigration from v2
Check the migration guide for detailed instructions on upgrading from v2.
License
MIT
Architecture (`docs/architecture.md`)
React Joyride Architecture
Data Flow
flowchart TD
Props --> useJoyride
useJoyride --> useTourEngine
useTourEngine --> Store
Store --> useSyncExternalStore
useSyncExternalStore --> TourRenderer
TourRenderer --> Step
TourRenderer --> Portal
Step --> Floater --> Portal
Portal --> Overlay & Loader & Beacon & TooltipTwo public APIs:
<Joyride>component โ SSR-safe wrapper that callsuseJoyrideinternallyuseJoyride(props)hook โ returns{ controls, failures, on, state, step, Tour }
State Machine
Two dimensions drive the tour:
Tour Status (overall)
stateDiagram-v2
[*] --> idle
idle --> ready : steps loaded
ready --> waiting : run=true, no steps
ready --> running : run=true + steps
waiting --> running : size > 0
running --> paused : stop()
paused --> running : start()
running --> finished : last step complete
running --> skipped : skip()
finished --> ready : reset()
skipped --> ready : reset()
paused --> ready : reset()idle: No steps loadedready: Steps loaded, waiting forrun=truewaiting:run=truebut steps loading async (transitions to RUNNING when size > 0)running: Tour activepaused: Stopped mid-tour viastop()(can resume withstart())finished: Tour completed normallyskipped: Tour ended viaskip()
Step Lifecycle (per step)
stateDiagram-v2
[*] --> init
init --> ready : target found / before resolved
ready --> beacon_before : beacon path
ready --> tooltip_before : skip beacon
beacon_before --> beacon : scroll done
beacon --> tooltip_before : user click
tooltip_before --> tooltip : scroll done
tooltip --> complete : user action
complete --> [*]init: Step starting, target check pending,beforehook runs hereready: Target found and visible, preparing renderbeacon_before: Beacon positioned, waiting for scroll to completebeacon: Showing pulsing beacontooltip_before: Tooltip positioned, waiting for scroll to completetooltip: Showing tooltip contentcomplete: Step done, transitioning to next
Beacon is skipped (goes straight to tooltip_before) when:
skipBeacon = truestep.placement = 'center'continuousmode AND action is NEXT or PREV
The *_before phases exist to hold rendering while scroll animation completes. The scroll effect watches for positioned=true during these phases, scrolls, then sets scrolling=false, which triggers the transition to the final beacon/tooltip phase.
Complete State Transition Sequence
A full multi-step tour from mount to finish:
mount: run=true, steps=[...]
โ start() โ status:running, lifecycle:init
โ tour:start event
โ [before hook runs if defined โ step:before_hook event โ waiting:true โ loader shown]
โ lifecycle: initโready (target check / before hook resolved)
โ step:before event (action: lastAction ?? update)
โ lifecycle: ready โ *_before (beacon_before or tooltip_before)
โ scrolling:true, positioned via Floating UI โ scroll animation
โ scroll:start event
โ scroll:end event โ scrolling:false
โ lifecycle: *_before โ beacon or tooltip
โ beacon event
โ User clicks beacon โ lifecycle: beacon โ tooltip_before โ tooltip
โ tooltip event
โ Focus trap activated
โ User clicks "Next" โ controls.next() โ lifecycle:complete, index+1
โ step:after event (reports previous index)
โ [after hook runs if defined โ step:after_hook event]
โ lifecycle: complete โ init (auto-advance in uncontrolled; controlled waits for parent to update stepIndex)
โ ... repeats for each step ...
โ Last step: index >= size โ status:finished
โ tour:end event (reports last step)
โ controls.reset() โ status:ready, index:0Controlled vs Uncontrolled
Uncontrolled (default): Store manages index internally. next()/prev()/go()/reset() work. Auto-advances past missing targets. Supports initialStepIndex for starting at a specific step.
Controlled (with stepIndex prop): Tour pauses at each step's COMPLETE lifecycle phase instead of auto-advancing. Parent manages the step index via onEvent callbacks. go() and reset() are disabled in controlled mode.
Store (`src/modules/store.ts`)
Class managing immutable state via plain objects. Snapshots are Object.freeze()d. Created once per component instance via useRef. Subscriptions drive React re-renders via useSyncExternalStore.
State Shape
{
action: Actions // What triggered the change
controlled: boolean // true if controlled prop is set
index: number // Current step index
lifecycle: Lifecycle // Step lifecycle phase
origin: Origin | null // What triggered the action (button_close, button_skip, button_primary, keyboard, overlay)
positioned: boolean // Whether floating element has been positioned (internal only)
scrolling: boolean // Whether scroll animation is in progress
size: number // Total steps count
status: Status // Tour status
waiting: boolean // Waiting for before hook or target polling
}positioned is internal โ stripped from public State type via omit(snapshot, 'positioned').
Store Methods
| Method | Description |
|---|---|
updateState(patch, forceIndex?) |
Merge patch, freeze snapshot, notify listeners. Controlled mode ignores index unless forceIndex=true |
subscribe(listener) |
Returns unsubscribe function. Used by useSyncExternalStore |
getSnapshot() / getServerSnapshot() |
Frozen state for useSyncExternalStore |
getState() |
Public state (without positioned) |
getEventState() |
Same as getState() โ for event payloads |
setPositionData(name, data) |
Stores beacon/tooltip PositionData. Sets positioned=true during *_before phases |
getPositionData(name) |
Returns cached position data |
cleanupPositionData() |
Clears both position caches (called on step init) |
setSteps(steps) |
Updates internal steps array and size |
applyTransitions() auto-promotes WAITING โ RUNNING when size > 0.
Event System
Single onEvent prop receives EventData and Controls:
type EventHandler = (data: EventData, controls: Controls) => void;
type EventData = TourData & {
error: Error | null; // Only for ERROR events
scroll: ScrollData | null; // Only for SCROLL_START/SCROLL_END
scrolling: boolean;
type: Events;
waiting: boolean;
}The controls parameter gives event handlers direct access to tour control methods (next, prev, start, stop, etc.).
sequenceDiagram
participant Tour
participant Step
participant Scroll
participant UI
Tour->>Step: tour:start
Step->>Step: step:before_hook
Step->>Step: step:before
Step->>Scroll: scroll:start
Scroll-->>Step: scroll:end
Step->>UI: beacon
Note over UI: user clicks
UI->>UI: tooltip
UI->>Step: step:after
Step->>Step: step:after_hook
Note over Step: repeat per step
Step->>Tour: tour:endAll events: tour:start, step:before_hook, step:before, scroll:start, scroll:end, beacon, tooltip, step:after, step:after_hook, tour:end, tour:status (on STOP/RESET), error:target_not_found, error
Controls (`src/hooks/useControls.ts`)
Extracted hook providing all tour control methods. Created once via useMemo, returned to consumers.
| Method | Effect |
|---|---|
close(origin?) |
RUNNING only. Sets action:CLOSE, index+1, lifecycle:COMPLETE |
go(nextIndex) |
RUNNING + uncontrolled only. Sets action:GO, index=nextIndex, lifecycle:COMPLETE. FINISHED if index >= size |
info() |
Returns current public state |
next() |
RUNNING only. Sets action:NEXT, index+1 (clamped to size), lifecycle:COMPLETE |
open() |
RUNNING only. Sets lifecycle:TOOLTIP_BEFORE to force-open tooltip |
prev() |
RUNNING only. Sets action:PREV, index-1 (clamped to 0), lifecycle:COMPLETE |
replay(origin?) |
RUNNING + lifecycle:TOOLTIP only. Sets action:REPLAY, lifecycle:COMPLETE. Index unchanged. Triggers full step re-entry through step:after โ step:before โ BEACON/TOOLTIP |
reset(restart?) |
Uncontrolled only. Sets index:0, lifecycle:INIT, status:READY (or RUNNING if restart) |
skip(origin?) |
RUNNING only. Sets action:SKIP, lifecycle:COMPLETE, status:SKIPPED |
start(nextIndex?) |
Sets action:START, lifecycle:INIT, status:RUNNING (or WAITING if no steps). Uses forceIndex=true |
stop(advance?) |
Not if FINISHED/SKIPPED. Sets action:STOP, lifecycle:COMPLETE, status:PAUSED. index+1 if advance |
All methods also reset positioned:false, scrolling:false, waiting:false.
Orchestration Hooks
useJoyride (`src/hooks/useJoyride.tsx`)
Public hook. Calls useTourEngine, wraps state (strips positioned), creates TourRenderer element, returns { controls, failures, on, state, step, Tour }.
useTourEngine (`src/hooks/useTourEngine.ts`)
Core orchestrator. Creates store once via useRef(createStore(mergedProps)), subscribes via useSyncExternalStore. Merges props with defaults via useMemoDeepCompare.
On mount: if run=true and steps exist and valid, calls controls.start().
Delegates to five sub-hooks:
useControlsโ tour control methodsuseEventEmitterโ centralized event emissionusePropSyncโ external prop change handlinguseLifecycleEffectโ state machine progressionuseScrollEffectโ scroll-to-target
useEventEmitter (`src/hooks/useEventEmitter.ts`)
Centralizes all onEvent calls into a single emitEvent(type, step, overrides?) function. Uses refs for onEvent and controls to keep callback identity stable. Composes event data from store.getEventState() + defaults (error: null, scroll: null) + step + type + overrides. Passes controls as second argument to onEvent.
Used by usePropSync, useLifecycleEffect, and useScrollEffect instead of calling onEvent directly.
usePropSync (`src/hooks/usePropSync.ts`)
Handles external prop changes:
- Steps change: Validates steps, calls
store.setSteps(steps). Fireserrorevent if invalid. - Run toggle:
run=trueโcontrols.start(),run=falseโcontrols.stop()
useLifecycleEffect (`src/hooks/useLifecycleEffect.ts`)
Five separate effects drive the state machine:
Effect 1: Action tracking (deps: [action])
Tracks NEXT/PREV/SKIP/CLOSE actions in lastAction ref for enriching callbacks.
Effect 2: Target resolution (deps: [index, lifecycle, status, store])
When RUNNING + step exists + lifecycle is INIT:
- Cleans up position data
- If step has
beforehook: setswaiting=true, firesSTEP_BEFORE_HOOK, runs the async hook with AbortController + timeout. On resolve/reject/timeout โ setslifecycle:READY, waiting:false - If no
beforehook: checks target visibility- Target visible โ
lifecycle:READY targetWaitTimeout=0โlifecycle:READY(proceed without target)- Otherwise โ polls at 100ms intervals, sets
waiting=true. On timeout โlifecycle:READY
- Target visible โ
Effect 3: Step presentation (deps: [lifecycle, store])
When lifecycle reaches READY:
- Target visible + from INIT โ fires
STEP_BEFOREevent - Target visible โ determines final lifecycle (BEACON or TOOLTIP via
hideBeacon()), checks if scroll needed, setslifecycle:*_BEFORE, scrolling:willScroll - Target not visible (RUNNING, not INIT/COMPLETE) โ fires
TARGET_NOT_FOUND, auto-advances in uncontrolled mode
Effect 4: Transitions + callbacks (deps: [lifecycle, positioned, scrolling, store])
*_BEFOREphase +scrolling=falseโ transitions to final BEACON/TOOLTIP- BEACON lifecycle โ fires
beaconevent - TOOLTIP lifecycle โ fires
tooltipevent - COMPLETE lifecycle (from TOOLTIP) โ fires
step:afterevent, runsafterhook (fire-and-forget), firesstep:after_hookevent
Effect 5: Tour flow (deps: [action, index, lifecycle, size, status, store])
- Action REPLAY + COMPLETE โ writes
lifecycle:INIT(works in both controlled and uncontrolled modes; bypasses the controlled-mode pause and the FINISHED branch on the last step to allow same-index re-entry) - No step + lifecycle INIT โ sets FINISHED
- Uncontrolled + COMPLETE + index < size โ auto-advances to INIT (controlled mode pauses here, waiting for parent to update
stepIndex) - COMPLETE + index >= size โ sets FINISHED
- Status โ FINISHED/SKIPPED โ fires
tour:end, callscontrols.reset(), clears lastAction - Status โ RUNNING (from IDLE/READY/PAUSED) โ fires
tour:start - Action โ STOP โ fires
tour:status, clears lastAction - Action โ RESET โ fires
tour:status, clears lastAction
useScrollEffect (`src/hooks/useScrollEffect.ts`)
Scrolls to target when lifecycle reaches *_before phases and positioned=true and scrolling=true:
- Custom scroll parent: first scrolls page to bring parent into view, then scrolls within parent
adjustForPlacement(): accounts for placement (top, left, right) to avoid tooltip being off-screen after scroll- Fires
scroll:startandscroll:endevents withScrollData(initial, target, element, duration) - Sets
scrolling:falseon completion, triggering Effect 4's*_BEFORE โ BEACON/TOOLTIPtransition - Cancels previous scroll if new one starts
Step Processing
Step Merging (`src/modules/step.ts`)
getMergedStep(props, currentStep) merges in two passes:
Pass 1 โ step merge:
defaultStep โ pick(props, basePropsFields) โ currentStepPass 2 โ options merge:
defaultOptions โ props.options โ pick(currentStep, optionFieldNames)Final overrides applied after merge:
locale: deep-mergeddefaultLocale โ props.locale โ step.localefloatingOptions: deep-mergeddefaultFloatingOptions โ props.floatingOptions โ step.floatingOptionsspotlightPadding: normalized to{ top, right, bottom, left }vianormalizeSpotlightPadding()styles: generated viagetStyles(step)
SharedProps fields picked from props: arrowComponent, beaconComponent, floatingOptions, loaderComponent, locale, styles, tooltipComponent
Step Hooks
Two hooks per step, defined as Options (globally or per-step):
before(data: TourData) โ Promise<void>: Blocks the tour at INIT phase. Sets waiting=true immediately; shows loader after loaderDelay (default 300ms). Capped by beforeTimeout (default 5000ms). On error/timeout, fires error event and proceeds.
after(data: TourData) โ void: Fire-and-forget after STEP_AFTER. Wrapped in try-catch โ user code errors don't break the tour.
Target Resolution & Tracking
Resolution (getElement() in src/modules/dom.ts):
- CSS selector string โ
document.querySelector() - HTMLElement โ used directly
- React ref โ
.current - Function โ called, returns element or null
Visibility (isElementVisible()): walks DOM tree checking display/visibility.
Position tracking (src/hooks/useTargetPosition.ts):
- ResizeObserver on target element
- Scroll/resize event listeners (passive)
- Returns
{ top, left, width, height, isFixed }with spotlightPadding applied - Disabled during scroll/waiting to avoid jitter
Rendering Pipeline
flowchart TD
J["Joyride (SSR guard)"] --> UJ[useJoyride]
UJ --> TR["TourRenderer (fragment)"]
TR --> S["Step"]
TR --> P["Portal element"]
S --> FT[useFocusTrap]
S --> F[Floater]
F --> P
P --> B["Beacon (BEACON phases)"]
P --> T["Tooltip (TOOLTIP phases)"]
P --> A[Arrow]
P --> L["Loader (waiting=true)"]
P --> O["Overlay (SVG spotlight)"]
style P fill:#f9f,stroke:#333All visible UI renders through Portal to the same portal element โ TourRenderer and Step produce only React fragments, not DOM elements. Floater wraps beacon/tooltip/arrow in , and TourRenderer wraps overlay/loader in . Both target the same element from usePortalElement.
Key Components
Floater (src/components/Floater.tsx):
- Wraps all rendered content (beacon or tooltip + arrow) in `` โ nothing renders inline
- Two
useFloating()instances: tooltip (full middleware) and beacon (offset only) - Tooltip middleware:
offset(includes spotlightPadding + arrow size),flip/autoPlacement,shift,arrow, plus custom middleware fromfloatingOptions - Center placement: custom middleware positions at viewport center, uses virtual reference element
- Strategy:
fixedwhenisFixed=trueor target has fixed/sticky positioning,absoluteotherwise - Reports position data to store via
setPositionData()on each calculation
Overlay (src/components/Overlay.tsx):
- SVG-based with
fillRule="evenodd"for spotlight cutout useTargetPositiontracks target rect via ResizeObserver + scroll/resize listeners- Two SVG paths: overlay path (full coverage with cutout) + cover path (fades out to reveal spotlight)
- Visible during TOOLTIP/TOOLTIP_BEFORE (continuous) or TOOLTIP only (non-continuous); also during waiting
- Hidden during BEACON/BEACON_BEFORE phases
Beacon (src/components/Beacon.tsx):
- Pulsing animation via CSS keyframes (injected once via style tag with existence check)
- Default: button with inner/outer animated spans
- Custom:
beaconComponentprop receives render props - Auto-focuses on mount if
shouldFocus=true
Tooltip (src/components/Tooltip/index.tsx):
- Generates button props (back, close, primary, skip) with handlers
- Primary button: "Next" (continuous + not last), "Last" (last step), "Close" (non-continuous)
- Delegates to DefaultTooltip (default) or
tooltipComponent(custom)
DefaultTooltip (src/components/Tooltip/DefaultTooltip.tsx):
- Default layout: title โ content โ footer (skip, back, primary buttons) โ close button
- Footer hidden if
buttonsarray has no footer buttons (back/primary/skip)
Arrow (src/components/Arrow.tsx):
- Default: SVG arrow element positioned by Floating UI middleware
- Custom:
arrowComponentprop receivesArrowRenderProps
Loader (src/components/Loader.tsx):
- Shown when
waiting=true(target polling orbeforehook) - Custom:
loaderComponentprop. Set tonullto disable
Portal (src/components/Portal.tsx):
createPortal(children, element)wrapper
usePortalElement (src/hooks/usePortalElement.ts):
- Creates `` in body, or uses provided
portalElement(selector or HTMLElement) - Cleans up auto-created div on unmount
Appendix
DOM Utilities (`src/modules/dom.ts`)
hasPosition(el, type?): Recursively checks for fixed/sticky positioninggetScrollParent(element): UsesscrollparentlibraryhasCustomScrollParent(element): Checks if scroll parent differs from documentgetScrollTo(element, offset): Calculates scroll positiongetScrollTargetToCenter(element): Centers element in viewportscrollTo(value, options): Smooth scroll viascrolllibrary; returns{ cancel, promise }scrollDocument(): Returns document scrolling elementgetDocumentHeight(median?): Takes max or median of 5 height measurementscanUseDOM(): SSR safety check
Focus Trap (`src/hooks/useFocusTrap.ts`)
- Intercepts Tab key within tooltip element
- Wraps focus forward/backward at tabbable element boundaries
- Focuses initial selector (
[data-action=primary]) on mount - Disabled when
step.disableFocusTrap=true - Restores previous focus on cleanup
Styles (`src/styles.ts`)
getStyles(step):
- Merges step styles with generated defaults
- Adaptive width:
min(step.width, window.innerWidth - 30) - Generates style objects (beacon, buttons, tooltip, overlay, arrow, etc.)
- Returns merged with user overrides
See src/defaults.ts for current default values.
Types (`src/types/`)
| File | Description |
|---|---|
common.ts |
Actions, Events, Lifecycle, Origin, Status, Placement, Locale, Options, Styles |
components.ts |
ArrowRenderProps, BeaconRenderProps, LoaderRenderProps, TooltipRenderProps |
events.ts |
EventData, EventHandler (data, controls), TourData, ScrollData |
floating.ts |
FloatingOptions, PositionData |
props.ts |
Props, SharedProps, UseJoyrideReturn, StepFailure |
state.ts |
Controls, State |
step.ts |
Step, StepMerged, StepTarget, SelectorOrElement |
utilities.ts |
PartialDeep, Simplify, SetRequired, ValueOf |
Key Dependencies
| Package | Purpose |
|---|---|
@floating-ui/react-dom |
Tooltip/beacon positioning (dual instances) |
@fastify/deepmerge |
Deep object merging (React element aware) |
@gilbarbara/hooks |
useMemoDeepCompare, useMount, usePrevious, useUpdateEffect, useWindowSize |
@gilbarbara/deep-equal |
Deep equality checking (store change detection) |
@gilbarbara/types |
Utility types |
scroll / scrollparent |
Smooth scrolling and scroll parent detection |
react-innertext |
Extract text from React elements (button labels) |
is-lite |
Type checking utilities |
--- METRICS ---
- Files Extracted: 2
- Estimated Token Budget: ~5913 tokens
- Canonical Reference: https://codewiki.google/github.com/gilbarbara/react-joyride