react-joyride Documentation

Create guided tours in your apps

7,880 stars TypeScript TypeScript 2 files ยท ~5,998 tokens #joyride#react#react-component#tooltip#tour
RAW Doc

Docker & Self-Hosted Deployment Verified OCI

Deploy react-joyride on any server, homelab, or cloud container platform.

1-Click Terminal Command
bash (docker run)
docker run -d --name react-joyride --restart always gilbarbara/react-joyride:latest
Configured Services & Ports
โš™๏ธ website
image: build locally
docker-compose.yml
yaml (docker-compose.yml)
services:
  website:
    build:
      context: ..
      dockerfile: website/Dockerfile
    environment:
      - NODE_ENV=production
    restart: unless-stopped
    networks:
      - joyride-network

networks:
  joyride-network:
    driver: bridge

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 steps and 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

bash
npm i react-joyride

Quick Start

Component API

tsx
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

tsx
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.

bash
npx skills add gilbarbara/react-joyride

Migration from v2

Check the migration guide for detailed instructions on upgrading from v2.

License

MIT


Architecture (`docs/architecture.md`)

React Joyride Architecture

Data Flow

mermaid
flowchart TD
    Props --> useJoyride
    useJoyride --> useTourEngine
    useTourEngine --> Store
    Store --> useSyncExternalStore
    useSyncExternalStore --> TourRenderer
    TourRenderer --> Step
    TourRenderer --> Portal
    Step --> Floater --> Portal
    Portal --> Overlay & Loader & Beacon & Tooltip

Two public APIs:

  • <Joyride> component โ€” SSR-safe wrapper that calls useJoyride internally
  • useJoyride(props) hook โ€” returns { controls, failures, on, state, step, Tour }

State Machine

Two dimensions drive the tour:

Tour Status (overall)

mermaid
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 loaded
  • ready: Steps loaded, waiting for run=true
  • waiting: run=true but steps loading async (transitions to RUNNING when size > 0)
  • running: Tour active
  • paused: Stopped mid-tour via stop() (can resume with start())
  • finished: Tour completed normally
  • skipped: Tour ended via skip()

Step Lifecycle (per step)

mermaid
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, before hook runs here
  • ready: Target found and visible, preparing render
  • beacon_before: Beacon positioned, waiting for scroll to complete
  • beacon: Showing pulsing beacon
  • tooltip_before: Tooltip positioned, waiting for scroll to complete
  • tooltip: Showing tooltip content
  • complete: Step done, transitioning to next

Beacon is skipped (goes straight to tooltip_before) when:

  • skipBeacon = true
  • step.placement = 'center'
  • continuous mode 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:

text
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:0

Controlled 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

typescript
{
  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:

typescript
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.).

mermaid
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:end

All 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 methods
  • useEventEmitter โ€” centralized event emission
  • usePropSync โ€” external prop change handling
  • useLifecycleEffect โ€” state machine progression
  • useScrollEffect โ€” 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). Fires error event 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:

  1. Cleans up position data
  2. If step has before hook: sets waiting=true, fires STEP_BEFORE_HOOK, runs the async hook with AbortController + timeout. On resolve/reject/timeout โ†’ sets lifecycle:READY, waiting:false
  3. If no before hook: 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

Effect 3: Step presentation (deps: [lifecycle, store])
When lifecycle reaches READY:

  • Target visible + from INIT โ†’ fires STEP_BEFORE event
  • Target visible โ†’ determines final lifecycle (BEACON or TOOLTIP via hideBeacon()), checks if scroll needed, sets lifecycle:*_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])

  • *_BEFORE phase + scrolling=false โ†’ transitions to final BEACON/TOOLTIP
  • BEACON lifecycle โ†’ fires beacon event
  • TOOLTIP lifecycle โ†’ fires tooltip event
  • COMPLETE lifecycle (from TOOLTIP) โ†’ fires step:after event, runs after hook (fire-and-forget), fires step:after_hook event

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, calls controls.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:start and scroll:end events with ScrollData (initial, target, element, duration)
  • Sets scrolling:false on completion, triggering Effect 4's *_BEFORE โ†’ BEACON/TOOLTIP transition
  • 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:

text
defaultStep โ†’ pick(props, basePropsFields) โ†’ currentStep

Pass 2 โ€” options merge:

text
defaultOptions โ†’ props.options โ†’ pick(currentStep, optionFieldNames)

Final overrides applied after merge:

  • locale: deep-merged defaultLocale โ†’ props.locale โ†’ step.locale
  • floatingOptions: deep-merged defaultFloatingOptions โ†’ props.floatingOptions โ†’ step.floatingOptions
  • spotlightPadding: normalized to { top, right, bottom, left } via normalizeSpotlightPadding()
  • styles: generated via getStyles(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

mermaid
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:#333

All 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 from floatingOptions
  • Center placement: custom middleware positions at viewport center, uses virtual reference element
  • Strategy: fixed when isFixed=true or target has fixed/sticky positioning, absolute otherwise
  • Reports position data to store via setPositionData() on each calculation

Overlay (src/components/Overlay.tsx):

  • SVG-based with fillRule="evenodd" for spotlight cutout
  • useTargetPosition tracks 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: beaconComponent prop 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 buttons array has no footer buttons (back/primary/skip)

Arrow (src/components/Arrow.tsx):

  • Default: SVG arrow element positioned by Floating UI middleware
  • Custom: arrowComponent prop receives ArrowRenderProps

Loader (src/components/Loader.tsx):

  • Shown when waiting=true (target polling or before hook)
  • Custom: loaderComponent prop. Set to null to 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 positioning
  • getScrollParent(element): Uses scrollparent library
  • hasCustomScrollParent(element): Checks if scroll parent differs from document
  • getScrollTo(element, offset): Calculates scroll position
  • getScrollTargetToCenter(element): Centers element in viewport
  • scrollTo(value, options): Smooth scroll via scroll library; returns { cancel, promise }
  • scrollDocument(): Returns document scrolling element
  • getDocumentHeight(median?): Takes max or median of 5 height measurements
  • canUseDOM(): 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):

  1. Merges step styles with generated defaults
  2. Adaptive width: min(step.width, window.innerWidth - 30)
  3. Generates style objects (beacon, buttons, tooltip, overlay, arrow, etc.)
  4. 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 ---