All posts

7 min read

How to Add a Product Tour to a React App (Step-by-Step)

Learn how to add a step-by-step product tour to your React or Next.js app with Intro.js, from a basic tour to refs, show-once logic and a reusable hook.

How to Add a Product Tour to a React App (Step-by-Step)

A product tour is one of the fastest ways to show new users where things are in your app. Instead of hoping people find your most important features on their own, a tour highlights each one in order and explains it in a sentence or two.

In this tutorial we'll add a product tour to a React app with Intro.js. We'll start with a basic tour, then cover the things that come up in every real React project: targeting elements with refs, cleaning up on unmount, showing the tour only once, letting users replay it, and making it work with Next.js server rendering.

1. Install Intro.js

Intro.js has no dependencies. Install it from npm:

npm install intro.js

It ships with its own TypeScript types, so there's nothing else to install.

2. Create your first tour

Import the library and its stylesheet, then start the tour from a useEffect once your component has rendered:

import { useEffect } from "react";
import introJs from "intro.js";
import "intro.js/introjs.css";

export default function Dashboard() {
  useEffect(() => {
    const tour = introJs.tour().setOptions({
      steps: [
        {
          title: "Welcome πŸ‘‹",
          intro: "Let's take a quick look around your dashboard.",
        },
        {
          element: "#new-project",
          intro: "Start here to create your first project.",
        },
        {
          element: "#search",
          intro: "Search across all of your projects and files.",
        },
        {
          element: "#settings",
          title: "You're all set",
          intro: "Change your preferences here at any time.",
        },
      ],
    });

    tour.start();

    return () => {
      tour.exit(true);
    };
  }, []);

  return (
    <div>
      <button id="new-project">New project</button>
      <input id="search" placeholder="Search" />
      <a id="settings" href="/settings">Settings</a>
    </div>
  );
}

A few things to note:

  • A step without an element is shown as a floating tooltip in the middle of the screen. That's a good fit for a welcome or closing message.
  • element can be a CSS selector, like "#search".
  • The cleanup function calls tour.exit(true). Without it, navigating away while the tour is open leaves the overlay on the screen. Passing true forces the exit even if you've added an onBeforeExit check.

The cleanup also matters in development. React's Strict Mode runs effects twice, so without a cleanup you would see two tours stacked on top of each other.

3. Target elements with refs instead of IDs

Adding IDs to elements just for a tour gets messy in a component-based app. Because element also accepts a DOM element, you can pass a React ref instead:

import { useEffect, useRef } from "react";
import introJs from "intro.js";
import "intro.js/introjs.css";

export default function Dashboard() {
  const newProjectRef = useRef(null);
  const searchRef = useRef(null);

  useEffect(() => {
    const tour = introJs.tour().setOptions({
      steps: [
        { element: newProjectRef.current, intro: "Create your first project here." },
        { element: searchRef.current, intro: "Find anything in seconds." },
      ],
    });

    tour.start();
    return () => {
      tour.exit(true);
    };
  }, []);

  return (
    <div>
      <button ref={newProjectRef}>New project</button>
      <input ref={searchRef} placeholder="Search" />
    </div>
  );
}

Refs are populated by the time useEffect runs, so ref.current points to the real element. If an element only appears after data loads, start the tour after that data is ready. For example, add the loading state to the effect's dependencies and return early while it's still loading.

4. Only show the tour once

Nobody wants to see the same tour every time they open your app. The simplest option is the built-in Don't Show Again checkbox:

introJs.tour()
  .setOptions({
    steps,
    dontShowAgain: true,
    dontShowAgainCookie: "dashboard-tour",
  })
  .start();

When the user ticks the checkbox, Intro.js stores a cookie and silently skips later start() calls. Give each tour its own dontShowAgainCookie name if you have tours on more than one page.

If you'd rather decide for the user, store a flag yourself when the tour ends:

const TOUR_KEY = "dashboard-tour-seen";

useEffect(() => {
  if (localStorage.getItem(TOUR_KEY)) return;

  const markSeen = () => localStorage.setItem(TOUR_KEY, "1");
  const tour = introJs.tour()
    .setOptions({ steps })
    .onComplete(markSeen)
    .onExit(markSeen);

  tour.start();
  return () => {
    tour.exit(true);
  };
}, []);

onComplete runs when the user finishes the last step. onExit also covers users who skip the tour, press Esc or click the overlay. You can also save this flag in your backend, so the tour doesn't reappear on a new device.

5. Let users replay the tour

Users often skip a tour and later wish they hadn't. A "Take the tour" button in your help menu solves that. Here's a small reusable hook that handles both cases:

import { useCallback, useEffect, useRef } from "react";
import introJs from "intro.js";
import "intro.js/introjs.css";

export function useTour(steps, { autoStart = false } = {}) {
  const tourRef = useRef(null);

  const start = useCallback(() => {
    tourRef.current?.exit(true);
    tourRef.current = introJs.tour().setOptions({ steps });
    tourRef.current.start();
  }, [steps]);

  useEffect(() => {
    if (autoStart) start();
    return () => {
      tourRef.current?.exit(true);
    };
  }, [autoStart, start]);

  return start;
}

And use it in any component:

const steps = [
  { title: "Welcome πŸ‘‹", intro: "Here's a quick tour of the app." },
  { element: "#new-project", intro: "Create your first project here." },
];

export default function HelpMenu() {
  const startTour = useTour(steps);
  return <button onClick={startTour}>Take the tour</button>;
}

Define steps outside the component (or wrap it in useMemo) so the hook doesn't restart the tour on every render.

6. Using Intro.js with Next.js

Intro.js works with the DOM, so it can't run on the server. If you import it at the top of a file that Next.js renders on the server, the build fails with document is not defined. That includes components marked with "use client", because they are still rendered on the server first.

The fix is to load the library inside useEffect, which only runs in the browser:

"use client";

import { useEffect } from "react";
import "intro.js/introjs.css";

export default function OnboardingTour({ steps }) {
  useEffect(() => {
    let tour;
    let cancelled = false;

    import("intro.js").then(({ default: introJs }) => {
      if (cancelled) return;
      tour = introJs.tour().setOptions({ steps });
      tour.start();
    });

    return () => {
      cancelled = true;
      tour?.exit(true);
    };
  }, [steps]);

  return null;
}

There's a bonus: Intro.js now loads as a separate chunk only on pages that use a tour, so it doesn't add weight to the rest of your app. The cancelled flag stops the tour from starting if the component unmounts before the import finishes.

7. Customize the look and behavior

Some tour options you'll probably want early on:

introJs.tour().setOptions({
  steps,
  showProgress: true,         // show a progress bar
  showBullets: false,         // hide the step dots
  exitOnOverlayClick: false,  // don't close on accidental clicks
  nextLabel: "Next β†’",
  prevLabel: "← Back",
  doneLabel: "Got it",
});

To match your brand, either override the .introjs-* CSS classes or start from one of the ready-made themes. For a right-to-left interface, add intro.js/introjs-rtl.css after the main stylesheet (see the RTL example).

If you need to fetch data or open a panel before a step is shown, onBeforeChange can return a Promise. Intro.js waits for it before moving on. See the async/await example.

Tips for a tour people actually finish

  • Keep it short. Three to five steps is usually enough. Introduce more advanced features later with hints next to the feature itself.
  • Explain the benefit, not the button. "Search across all your files" is more useful than "This is the search bar."
  • Give users a way out. Always let people skip, and make it easy to replay the tour later.
  • Measure it. Track onChange, onComplete and onExit events in your analytics to see where people drop off.

For more on designing tours, read our 5 tips for more engaging product tours.

Wrapping up

Adding a product tour to a React app takes a few lines of code. Start the tour in useEffect, clean it up on unmount, use refs for your steps and store whether the user has seen it. In Next.js, import Intro.js dynamically. From there, you can tune the options and styling to fit your product.

Prefer a component API? The community-maintained intro.js-react wrapper lets you declare steps and hints as React components. To go further, see the full Intro.js documentation and tour API reference.

Keep reading

View all posts