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.
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:
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:
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:
target: document.getElementById("welcome-card")
But the standard is to use CSS selector strings, like:
target: '[data-tour="element"]'
How it works is: when the step becomes active, Joyride internally calls the browser's native API:
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:
<div
id="my-card"
data-joy-tour="welcome-banner"
data-category="financial"
>
</div>
If you omit the data- prefix and write:
<div joy-tour="welcome-banner">
the attribute will not appear in the dataset:
dataset.joyTour
placement
placement defines where the tooltip bubble is positioned relative to the target element.
It accepts the following options:
top
bottom
left
right
For example:
placement: "bottom"
draws the tooltip directly below the target.
There are also:
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:
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:
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:
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.
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.
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:
controls.next()
Force the tour to advance to the next step.
controls.prev()
Force the tour to go back.
controls.close()
Close the active step.
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:
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:
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
"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.