Software Engineering

React Joyride & Next.js Onboarding Tour implementation guide

I have written this guide to remind myself of every architectural decision and implementation step of the onboarding tour system so that I can clearly teach others how to build it from scratch.

KUZUE
August 28, 2026
8 min read
12 views

An onboarding tour is an overlay layer that guides users through a web application by pointing at specific UI elements and showing explanation bubbles.

To build one, you need three things:

The Targets

Elements in your HTML that the bubble will point to.

The Steps Config

An array of instructions telling the tour which element to target and what text to show.

The Tour Engine

The library (React Joyride) that calculates where the elements are on the screen, draws the translucent background backdrop cutout, and positions the tooltip card dynamically.


Step 1: Installation

Install React Joyride:

bash
npm install react-joyride --legacy-peer-deps

The --legacy-peer-deps flag is used to tell the package manager:

"I know that React version differs; bypass strict checks and install it anyway."

Once installed, the library exposes several core modules:

Joyride — Named Component

The core React component engine that coordinates overlays, positioning, and clicks.

EventData — TypeScript Type

The type interface representing the event object passed to the tour coordinator on every step transition or action, like skipping, closing, or clicking next.

STATUS — Constants Namespace

Pre-defined state strings, like STATUS.FINISHED and STATUS.SKIPPED, that allow you to check when the tour has ended.

Options & Styles — Type Interfaces

Structuring rules for configuring background overlays, font dimensions, button classes, and coordinates.


Step 2: The Steps

The steps represent an array of the information you want to show.

For example:

tsx
import { Step } from "react-joyride";

export const dashboardTourSteps: Step[] = [
  {
    target: '[data-tour="welcome-banner"]',
    // It tells the page: Search the page for any HTML tag
    // that has an attribute named data-tour set exactly
    // to the value "welcome-banner".

    title: "Welcome to LedgerLite! 👋",

    content:
      "Let's take a quick 1-minute tour to help you get familiar with your financial dashboard and business tracker.",

    placement: "bottom",

    skipBeacon: true,
    // Auto-open without clicking pulsating dot
  },
];

target

The target parameter is the coordinates anchor.

It tells the tour engine exactly which element on the screen to highlight and point to.

The target tells React Joyride which element to highlight.

We can also use a direct DOM element:

js
target: document.getElementById("welcome-card")

But the standard is to use CSS selector strings, like:

js
target: '[data-tour="element"]'

How it works is: when the step becomes active, Joyride internally calls the browser's native API:

js
document.querySelector(target)

When you use the data- prefix, the browser automatically grants you access to a special JavaScript feature: the dataset object.

Every HTML element has a .dataset property that automatically collects all attributes starting with data- and camel-cases them.

You can name the attribute whatever you want, but you should always keep the data- prefix, e.g., data-joy-tour.

The data- prefix keeps your HTML compliant with W3C web standards and automatically places the attribute inside JavaScript's native element.dataset object, making it much cleaner to read and write in code.

For example:

html
<div
  id="my-card"
  data-joy-tour="welcome-banner"
  data-category="financial"
>
</div>

If you omit the data- prefix and write:

html
<div joy-tour="welcome-banner">

the attribute will not appear in the dataset:

js
dataset.joyTour

placement

placement defines where the tooltip bubble is positioned relative to the target element.

It accepts the following options:

text
top
bottom
left
right

For example:

js
placement: "bottom"

draws the tooltip directly below the target.

There are also:

text
top-start
top-end
bottom-start
bottom-end

and so on.

start aligns the left edges of the tooltip and target.

end aligns the right edges.

Smart Placement

If you set:

js
placement: "auto"

the engine will check the user's screen size.

Center

center places the tooltip card dead-center in the middle of the screen, behaves like a pop-up modal, and dims everything else.

This is excellent for welcome steps or ending summaries where you are not pointing at any specific element.


Beacon

A Beacon is the small, pulsing indicator, usually a red dot, that appears over a target element before the step's tooltip opens.

The user must click the beacon to "trigger" the tooltip card to open.

By default, the user must click the beacon for the tooltip to open.

But if:

jsx
continuous={true}

once the user clicks the first beacon and starts the tour, Joyride will automatically skip subsequent beacons.

Clicking Next will immediately open the next tooltip.

Standard practice is to use:

js
skipBeacon: true

on the very first step so the welcome card pops open immediately, while letting subsequent steps transition smoothly.


Step 3: Create the Tour Component

Create a component that returns the Joyride component.

Because of SSR, since we want to avoid window is not defined, we use lazy loading import() to import React Joyride.

The Joyride component requires many props to make it work. All are optional; only steps is mandatory.


run

run is a boolean that holds state for when to run.

js
run={run}

onEvent

The onEvent prop is React Joyride's central listener.

It functions like a live event feed, calling our handler function every time the tour transitions, displays tooltips, gets closed, or encounters layout errors.

It passes us a state report (data) and a control joystick (controls) so we can write custom logic to react to the user's progress.

ts
const handleJoyrideCallback = (
  data: EventData,
  controls: Controls
) => {
  // ...
};

data — The State Report

An object telling you what just happened, which step is active, and how the user triggered it.

controls — The Joystick

An object containing methods that allow you to programmatically override the tour.

It contains functions like:

text
controls.next()

Force the tour to advance to the next step.

text
controls.prev()

Force the tour to go back.

text
controls.close()

Close the active step.

text
controls.skip()

Terminate the tour immediately.


locale

The locale prop is used to customize or translate button labels in the tour bubble.

By default, React Joyride tooltips render buttons with generic English labels:

  • The button to go to the next step is labeled: "Next"
  • The button to go back a step is labeled: "Back"
  • The button to close/skip the tour is labeled: "Skip"
  • The button on the very last step is labeled: "Last"

We can customize them:

jsx
locale={{
  last: "Finish", // Overrides "Last" -> "Finish" on the final step
  skip: "Skip",
  back: "Go Back",
  next: "Continue",
  close: "Dismiss",
}}

options

The options prop acts as the global theme and logic configuration.

Instead of styling each card manually, we set colors, matching our brand teal, overlays, and progress trackers in this single object.

If we need a specific step to behave or look differently, we can override any of these options directly inside that step's configuration.


styles

The styles prop takes a nested object where each key represents an HTML sub-component of the tour, and the values are standard CSS properties written in camelCase, e.g., borderRadius instead of border-radius.

The styles prop allows us to write detailed inline CSS overrides for every sub-component of the tour.

We use it to set premium button paddings, clean typography hierarchies, and card shadows, ensuring that our onboarding tooltips look like custom-designed components rather than generic third-party plugins.


Client-Side Mounting Guard

Then we set up a guard so that once the component enters the browser:

tsx
const [mounted, setMounted] = useState(false);

useEffect(() => {
  setMounted(true);
}, []);

if (!mounted) return null;

This ensures that the component does not attempt to use browser-specific functionality before it has mounted on the client.


Complete TourManager Component

tsx
"use client";

import { useEffect, useState } from "react";
import dynamic from "next/dynamic";
import { EventData, STATUS } from "react-joyride";

// Dynamically import Joyride with SSR turned off
// to prevent Next.js server pre-rendering hydration failures
const Joyride = dynamic(
  () => import("react-joyride").then((mod) => mod.Joyride),
  { ssr: false }
);

interface TourManagerProps {
  steps: any[];
}

export default function TourManager({ steps }: TourManagerProps) {
  const [mounted, setMounted] = useState(false);
  const [run, setRun] = useState(false);

  // Guard to ensure component is fully mounted in the client browser
  useEffect(() => {
    setMounted(true);

    // Auto-run the tour if the user has not completed it yet
    const completed = localStorage.getItem(
      "ledgerlite_onboarding_completed"
    );

    if (!completed) {
      setRun(true);
    }
  }, []);

  // Listen for manual restart requests from other components
  // (like WelcomeGreeting)
  useEffect(() => {
    const handleRestart = () => {
      localStorage.removeItem("ledgerlite_onboarding_completed");
      setRun(true);
    };

    window.addEventListener(
      "ledgerlite-tour-restart",
      handleRestart
    );

    return () => {
      window.removeEventListener(
        "ledgerlite-tour-restart",
        handleRestart
      );
    };
  }, []);

  if (!mounted) return null;

  const handleJoyrideCallback = (data: EventData) => {
    const { status } = data;

    const finishedStatuses: string[] = [
      STATUS.FINISHED,
      STATUS.SKIPPED,
    ];

    // If tour finished or skipped, trigger completion state save
    if (finishedStatuses.includes(status)) {
      setRun(false);

      localStorage.setItem(
        "ledgerlite_onboarding_completed",
        "true"
      );
    }
  };

  return (
    <Joyride
      steps={steps}
      run={run}
      continuous={true}
      // Enables Next buttons instead of forcing user clicks on beacons
      onEvent={handleJoyrideCallback}
      locale={{
        last: "Finish",
        skip: "Skip",
      }}
      options={{}}
      styles={{}}
    />
  );
}

Comments (0)

No comments yet. Be the first to share your thoughts.