A spotlight highlighting one exhibit in a dim museum gallery with a guided tour route, a gray cat watching from a bench.

React Joyride: Take Your Users on a Guided Adventure

The Gray Cat
The Gray Cat
0 views

Every product team eventually faces the same quiet failure: a new user signs up, opens the app, stares at an unfamiliar interface, and leaves before discovering the feature that would have made them stay. React Joyride exists to close that gap. It turns your app into a guided experience where each important element gets its own moment in the spotlight, with a tooltip that explains what it does and a path that leads users from one feature to the next.

The library handles the genuinely hard parts of building a tour: dimming the page while highlighting a target element, positioning a tooltip without it spilling off-screen, scrolling targets into view, trapping keyboard focus, and wiring up ARIA attributes so the whole thing is accessible. You describe your tour as a declarative array of steps, point each one at an element by CSS selector, and write the content. Joyride takes care of the rest. It is one of the oldest and most-downloaded tour libraries in the React ecosystem, with around a million weekly npm downloads, and it stays squarely in React idioms rather than wrapping a framework-agnostic core.

This article covers the current v3 line, which was a substantial rewrite. If you have seen older tutorials using a default <Joyride> export with a callback prop and no hook, that is the v2 API. Version 3 introduced a named export, a new useJoyride hook, a controls object for programmatic steering, and per-step before / after hooks. Everything below reflects v3.

What You Get Out of the Box

React Joyride is more than a styled tooltip. A few of its standout capabilities:

  • Declarative steps. Describe the whole tour as an array. Each step points at an element by CSS selector or DOM node and carries its own content, title, and placement.
  • Spotlight and beacon. The overlay dims the page and cuts a hole around the target. An optional pulsing beacon invites the user to click before the tooltip appears.
  • Two flow modes. A continuous tour gives you Next and Back buttons across a single flow, while the default mode shows one beacon-to-tooltip per step that the user re-triggers.
  • Automatic scrolling and lifecycle phases. Targets scroll into view, and internal *_before phases hold rendering until the scroll animation finishes so tooltips never point at the wrong place.
  • Accessibility built in. Focus trapping, keyboard navigation, and ARIA attributes come standard.
  • Deep customization. Override every color and element through a styles object, relabel buttons with locale, or swap in entirely custom Tooltip, Beacon, Overlay, and Spotlight components.
  • A rich event system. Named EVENTS, STATUS, and ACTIONS constants let you react to every state change, which is how you persist that a user finished onboarding.

Getting It Into Your Project

Install with your package manager of choice:

npm install react-joyride
yarn add react-joyride
pnpm add react-joyride

Joyride supports React 16.8 and up, all the way through React 19, and it is SSR-safe, so it slots into Next.js and other server-rendered frameworks without ceremony.

Your First Tour in Two Minutes

The fastest way to a working tour is the <Joyride> component. It is an SSR-safe wrapper that calls the useJoyride hook internally, so you get a tour with almost no setup. Note the named import in v3.

import { Joyride } from 'react-joyride';
import type { Step } from 'react-joyride';

const steps: Step[] = [
  {
    target: '.sidebar-projects',
    content: 'All your projects live here. Click any one to open it.',
  },
  {
    target: '.new-task-button',
    content: 'And this is how you create a brand-new task.',
  },
];

export function App() {
  return (
    <div>
      <Joyride run steps={steps} />
      {/* the rest of your app */}
    </div>
  );
}

Each step needs only a target and content. The target is a CSS selector that matches an element already in your DOM, and content can be a plain string or any JSX. The run prop starts the tour immediately. By default, each step shows a beacon first, and clicking it reveals the tooltip.

Turning It Into a Continuous Walkthrough

A single beacon per step is great for contextual hints, but onboarding usually wants one connected flow with Next and Back buttons. Add the continuous prop, and consider relabeling the buttons and showing progress so users know how far they have to go.

import { Joyride } from 'react-joyride';
import type { Step } from 'react-joyride';

const steps: Step[] = [
  {
    target: '.dashboard-header',
    title: 'Welcome aboard',
    content: 'This is your dashboard. Let us show you around.',
    placement: 'bottom',
  },
  {
    target: '.metrics-panel',
    content: 'Your key numbers update here in real time.',
  },
  {
    target: '.invite-team',
    content: 'When you are ready, invite your teammates from here.',
    placement: 'left',
  },
];

export function App() {
  return (
    <Joyride
      continuous
      run
      showProgress
      showSkipButton
      steps={steps}
      locale={{
        back: 'Back',
        close: 'Got it',
        last: 'Finish',
        next: 'Next',
        skip: 'Skip tour',
      }}
    />
  );
}

The placement property nudges the tooltip to a side of the target, and a special center value renders a modal-style step with no spotlight, which is perfect for a welcome or farewell message. showProgress adds a step counter to the Next button, and showSkipButton gives users an exit.

Taking the Wheel With the Hook

The <Joyride> component is convenient, but the real power in v3 is the useJoyride hook. It hands you a controls object for driving the tour programmatically, the current state, and a Tour element to drop into your JSX.

import { useJoyride } from 'react-joyride';
import type { Step } from 'react-joyride';

const steps: Step[] = [
  { target: '.sidebar-projects', content: 'Your projects live here.' },
  { target: '.new-task-button', content: 'Create a task from here.' },
];

export function App() {
  const { controls, state, Tour } = useJoyride({ steps });

  return (
    <div>
      <button onClick={() => controls.start()}>
        {state.status === 'finished' ? 'Replay tour' : 'Start tour'}
      </button>
      {Tour}
      {/* the rest of your app */}
    </div>
  );
}

The hook returns { controls, failures, on, state, step, Tour }. The controls object is your remote control: start(nextIndex?) begins or resumes, stop() pauses, next() and prev() move between steps, go(index) jumps to a specific step, skip() ends early, and reset() returns to the beginning. Version 3.1.0 added replay(), which re-runs the current step without changing the index. This hook-driven approach replaces the brittle useEffect-plus-stepIndex patterns that v2 users often resorted to.

Reacting to the Journey

Knowing when a user finishes or skips a tour is what lets you persist their onboarding state so they never see it twice. Joyride emits an event on every state change, and it ships named constants so you never have to memorize magic strings. With the component API, hook into the callback prop; the hook API exposes the same stream through on.

import { useState } from 'react';
import { Joyride, EVENTS, STATUS } from 'react-joyride';
import type { CallBackProps, Step } from 'react-joyride';

const steps: Step[] = [
  { target: '.dashboard-header', content: 'Welcome to your dashboard.' },
  { target: '.metrics-panel', content: 'Your numbers update live here.' },
];

export function OnboardingTour() {
  const [run, setRun] = useState(true);

  function handleCallback(data: CallBackProps) {
    const { status } = data;

    if (([STATUS.FINISHED, STATUS.SKIPPED] as string[]).includes(status)) {
      setRun(false);
      localStorage.setItem('onboardingDone', 'true');
    }
  }

  return (
    <Joyride
      continuous
      run={run}
      steps={steps}
      callback={handleCallback}
    />
  );
}

The callback payload includes action, index, status, type, and the current step. The STATUS constants cover the whole lifecycle, IDLE through RUNNING, PAUSED, FINISHED, and SKIPPED, while EVENTS constants like STEP_AFTER and TARGET_NOT_FOUND describe individual transitions. Detecting FINISHED or SKIPPED is the canonical way to mark onboarding complete.

Handling Steps That Are Not There Yet

Real apps are not static. A step might point at an element behind a modal, inside a tab that has not been opened, or on a route that has not loaded. In v3, the recommended answer is the per-step before hook rather than juggling stepIndex from a useEffect. A before hook runs before its step renders, giving you a place to open that drawer, switch tabs, or fetch data.

import { useJoyride } from 'react-joyride';
import type { Step } from 'react-joyride';

const steps: Step[] = [
  { target: '.menu-button', content: 'Open the menu here.' },
  {
    target: '.settings-panel',
    content: 'These are your settings.',
    before: async () => {
      // Make sure the panel exists before the step tries to highlight it
      openSettingsDrawer();
      await waitForElement('.settings-panel');
    },
  },
];

export function App() {
  const { controls, Tour } = useJoyride({ steps });

  return (
    <div>
      <button onClick={() => controls.start()}>Start tour</button>
      {Tour}
    </div>
  );
}

In uncontrolled mode, which is the default, the internal store owns the step index, auto-advances past targets it genuinely cannot find, and lets next, prev, go, and reset all work freely. There is a controlled mode where you own stepIndex through a prop, but the v3 docs explicitly advise using it sparingly. For most dynamic tours, uncontrolled mode plus before hooks is cleaner and far less fiddly than manually shuttling the index around.

Dressing the Tour to Match Your Brand

A tour that looks bolted on undermines the polish it is supposed to demonstrate. The styles prop lets you override a small options palette that cascades across the whole tour, plus targeted styles for individual elements like the tooltip, buttons, beacon, and spotlight.

import { Joyride } from 'react-joyride';

export function BrandedTour({ steps }) {
  return (
    <Joyride
      continuous
      run
      steps={steps}
      styles={{
        options: {
          primaryColor: '#7c3aed',
          textColor: '#1f2937',
          backgroundColor: '#ffffff',
          overlayColor: 'rgba(15, 23, 42, 0.6)',
          arrowColor: '#ffffff',
          zIndex: 10000,
          width: 360,
        },
        tooltipContainer: {
          textAlign: 'left',
        },
        buttonNext: {
          borderRadius: 8,
        },
      }}
    />
  );
}

When inline overrides are not enough, you can supply custom components to replace the Tooltip, Beacon, Overlay, or Spotlight with your own React components, giving you total control of look and behavior. There is also a floaterProps escape hatch that passes options straight down to the underlying positioning engine, which in v3 is built on @floating-ui/react-dom. That is your tool for taming awkward scroll containers, sticky headers, and modals.

A Few Things to Keep in Mind

Joyride is mainstream and production-ready, but a few trade-offs are worth knowing up front. At roughly 25 KB gzipped, it is heavier than a vanilla alternative like driver.js; that weight buys you React-native ergonomics and deep customization, but if your bundle budget is tight and you do not need them, it is a real cost. Because steps key off CSS selectors, targets must actually exist in the DOM, which is exactly why the before hook matters for dynamic UIs. Multi-page tours that span routes require you to coordinate Joyride with your router yourself. And if you are migrating from v2, budget time for the API churn: the named export, the hook, and the new controls all changed.

Where Joyride Earns Its Keep

React Joyride is the default choice for React-specific guided tours for a reason. It pairs a clean, declarative step model with the v3 hook API and a mature event system, so you can ship anything from a gentle first-run welcome to a multi-step feature-discovery walkthrough without reinventing positioning, scrolling, focus management, and accessibility. The component API gets you running in minutes, the useJoyride hook hands you programmatic control when you need it, and before hooks keep dynamic UIs honest. If you are building onboarding that users actually finish, this is a tour guide worth hiring.