# React Product Tour: Spotlight, Phone Dock, Hotspots

> Build a React product tour that spotlights the real UI, docks at the bottom on phones and remembers who finished it, plus when a hotspot is the better choice.

- Author: [Serban Rusu](https://wingo-ui.com/blog/authors/serban), Founder of Wingo UI
- Published: Oct 19, 2026
- Category: [Component guides](https://wingo-ui.com/blog/category/components)
- Reading time: 9 min
- Canonical: https://wingo-ui.com/blog/react-product-tour

## TL;DR

A good React product tour dims the page around the real element, explains it in a card that keeps focus, and moves on when people press the real button. On a phone the card should dock at the bottom with the target scrolled above it, and the tour should remember who finished or skipped it. For a single new feature, a pulsing hotspot that people open when they want is usually better than a tour.

A React product tour looks simple in a demo: a box around a button, a card with Next, five steps, done. Then it meets a real app. The target is inside a closed menu, the card covers the thing it describes on a 390px screen, Tab wanders off into the page behind the overlay, and everyone who reloads gets the tour again. This guide covers what a production tour needs, how the Wingo UI [Tour](https://wingo-ui.com/components/tour) handles the spotlight, the phone layout and saved progress, and when you should not build a tour at all.

## What should a React product tour handle?

Seven things. A React guided tour that only highlights elements on a wide screen covers the first one:

1. **A spotlight on the real UI.** Dim the page, cut a rounded hole around the target, and follow it when the page scrolls or resizes.
2. **Targets that are not there yet.** A step can point at an item inside a menu or a tab that has to open first.
3. **Interactive steps.** "Press New invoice" should move on when people press the real New invoice button.
4. **Keyboard and screen reader support.** Focus in the card, arrow keys to page, Escape to leave, focus back where it was.
5. **A phone layout.** A card floating next to a 44px button covers half a phone screen.
6. **Memory.** Finished or skipped tours do not come back on the next visit.
7. **A way back in.** A Help menu item or a shortcut that replays it.

## How do the React tour libraries compare?

React Joyride is the established free React tour library, Driver.js is a small library without React bindings, and Shepherd.js is free only for non-commercial work. Those are the names that come up when you look for a React Joyride alternative. Facts come from each project's README or site, as of October 2026.

| Option | What it is | License | React | Notes from the docs |
|---|---|---|---|---|
| [React Joyride](https://github.com/gilbarbara/react-joyride) | npm package, `<Joyride>` component and `useJoyride` hook | MIT | 16.8 through 19, per the README | Lists focus trapping, keyboard navigation, ARIA support and SSR safety |
| [Driver.js](https://github.com/kamranahmedse/driver.js) | Framework-agnostic JavaScript package | MIT | No React bindings, you call `driver()` yourself | Zero dependencies and about 5 kB gzipped, per its README; highlights, popovers and tours |
| [Shepherd.js](https://shepherdjs.dev/) | Framework-agnostic package | AGPL-3.0, commercial license sold separately | Works with React, per its site | Free for open source, personal and non-commercial projects |
| [Wingo UI Tour](https://wingo-ui.com/components/tour) | Pro source component, `npx wingo-ui@latest add tour` | [Wingo UI license](https://wingo-ui.com/legal/license) | 19 | Docked phone card with swipe paging, hotspots, persisted outcome |

React Joyride is the safe pick if you want a free, maintained package and plan to handle phones yourself. Driver.js fits a project that is not all React. The rest of this guide covers the Wingo UI Tour, which ships as source in `components/ui`.

## What props does a React product tour need?

One: `steps`. Each step has an `id`, a `title`, optional `content` and `media`, and a `target`, which can be a CSS selector, a function that returns an element, or a ref. A step without a target, or one whose target does not show up within one second, gets a centered card, which is how you write a welcome and a closing step.

```bash
npx wingo-ui@latest add tour
```

```tsx
"use client";

import { useRef } from "react";
import { Button } from "@/components/ui/button";
import { Tour, type TourStep } from "@/components/ui/tour";

export function ClientsPage() {
  const search = useRef<HTMLInputElement>(null);
  const invoice = useRef<HTMLButtonElement>(null);

  const steps: TourStep[] = [
    { id: "welcome", title: "Welcome to Northwind CRM", content: "Here is where everything lives. It takes 30 seconds." },
    { id: "search", target: search, title: "Find any client", content: "Type a name, an EIN or a city." },
    { id: "filters", target: "[data-tour='filters']", title: "Saved filters", content: "Keep the combinations you use often." },
    { id: "invoice", target: invoice, title: "Issue an invoice", content: "Press New invoice to continue.", advanceOn: "click" },
    { id: "done", title: "You are all set", content: "Replay this tour any time from the Help menu." },
  ];

  return (
    <Tour steps={steps} persistKey="northwind-crm:welcome-tour" autoStart shortcut="shift+h">
      <input ref={search} placeholder="Search clients" />
      <div data-tour="filters">{/* saved filter chips */}</div>
      <Button ref={invoice}>New invoice</Button>
    </Tour>
  );
}
```

The Tour renders its children in place and puts the overlay in a portal. Each step first scrolls its target into view, then the spotlight glides there on a spring and the card fades in on the first side with room for it (`side="auto"` tries below, then above, then the wider of left and right). Next you will reach for `showProgress` (`dots`, `fraction`, `bar` or `none`), `size` (`sm` for 288px cards, `md` for 340px), `tone`, `spotlightPadding` and `spotlightRadius` (also settable per step), and `labels` for every string.

Start the tour below and follow it to step four: it waits until you press the real New invoice button. Right arrow and Enter go forward, Left goes back, Escape leaves.

> Live demo (Tour): Press Start the tour (or Shift + H), follow the spotlight to step four and press the real New invoice button to move on. Try it at [Tour](https://wingo-ui.com/components/tour) and install it with `npx wingo-ui@latest add tour`.

## How do steps wait for menus and real clicks?

Three step options cover it. `interactive: true` lets clicks and typing reach the highlighted element, so people can type into the search box while the card explains it. `advanceOn: "click"` implies interactive and calls next when the target is clicked. `beforeEnter` runs before the step shows and is awaited for up to three seconds, which is enough to open a menu or switch a tab so the target exists:

```tsx
{
  id: "export",
  target: exportItemRef,
  title: "Export to Excel",
  content: "The filtered list goes into an .xlsx file.",
  side: "right",
  beforeEnter: async () => {
    setMenuOpen(true);
    await new Promise((resolve) => requestAnimationFrame(resolve));
  },
},
```

Close the menu again in the next step's `beforeEnter` and in `onOpenChange` when the tour ends, or it stays open after a skip. If a target is removed while its step is showing, the card falls back to the center instead of pointing at nothing.

## How should a product tour work on a phone?

Dock the card at the bottom. Below 768px the Tour does that by default (`mobileLayout="docked"`): the card becomes a sheet above the safe area, its Back and Next buttons are 48px tall, it rides above the on-screen keyboard when one opens, and a target outside the top 55% of the screen scrolls up into it, so the card does not cover it. A swipe left on the card goes to the next step and a swipe right goes back.

The reason is the thumb. A popover anchored to a button at the bottom of a phone screen sits on top of the hand that has to tap Next, and a popover anchored near the top pushes Next out of reach. The same rule drives our [React bottom sheet guide](https://wingo-ui.com/blog/react-bottom-sheet-for-the-web). Pass `mobileLayout="popover"` if you really want the desktop card on phones, or `docked` to dock it at every width, which is what you want inside a phone mockup together with `container`.

> Live demo (Tour): The same tour with docked on: start it and the card docks at the bottom of your window. Drag or swipe the card sideways to page through the steps. Try it at [Tour](https://wingo-ui.com/components/tour) and install it with `npx wingo-ui@latest add tour`.

## How do I remember who finished the tour?

Give it a `persistKey`. The Tour then stores `completed` or `skipped` in local storage, `autoStart` (which opens the tour 600ms after hydration) leaves a finished tour alone, `start()` still replays it, and `reset()` forgets it. That is enough for one device. For a React onboarding tour shown to signed-in users, save the outcome on the server too, so a new laptop does not replay it:

```tsx
"use client";

import { Button } from "@/components/ui/button";
import { Tour, useTour, type TourStep } from "@/components/ui/tour";

function saveTour(status: "completed" | "skipped") {
  void fetch("/api/me/tours/welcome", {
    method: "PUT",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ status }),
  });
}

function ReplayTour() {
  const tour = useTour();
  if (!tour) return null;
  return (
    <Button variant="ghost" size="sm" onClick={() => tour.start()}>
      Replay tour
    </Button>
  );
}

export function AppShell({ steps, seenTour }: { steps: TourStep[]; seenTour: boolean }) {
  return (
    <Tour
      steps={steps}
      autoStart={!seenTour}
      onComplete={() => saveTour("completed")}
      onSkip={() => saveTour("skipped")}
    >
      <ReplayTour />
      {/* the rest of the app */}
    </Tour>
  );
}
```

`useTour()` gives anything inside the Tour its API plus `open`, `step`, `count` and `status`, so the replay button can sit in a header or a Help menu. For code outside the Tour's children, the `setApi` prop hands you the same `start`, `next`, `previous`, `goTo`, `skip`, `finish` and `reset`. Only one tour shows at a time: a second one that opens waits for the first to close.

## How do I make a product tour accessible?

Treat each card as a dialog. The Tour's card has `role="dialog"`, `aria-modal`, and a label and description tied to the step's title and content. Tab stays inside the card without locking the page, focus returns to where it was when the tour ends, and a polite live region announces "Step 2 of 5: Find any client" on every step and "Tour closed" or "Tour completed" at the end. The highlighted element gets `aria-describedby` pointing at the step's text for as long as the step shows, so a screen reader reading the real button also hears what the tour says about it.

Clicks on the dimmed area do nothing by default (`closeOnOverlayClick={false}`), so a stray tap on a phone does not end the tour. By default every card has Skip or a close button, and Escape skips from anywhere. Under reduced motion the spotlight jumps instead of gliding and the cards fade; the [prefers-reduced-motion guide](https://wingo-ui.com/blog/prefers-reduced-motion-react) explains the hook behind it.

## When is a hotspot better than a tour?

When you want to point at one thing. Nielsen Norman Group's [Onboarding Tutorials vs. Contextual Help](https://www.nngroup.com/articles/onboarding-tutorials/) (Page Laubheimer, February 2023) puts it bluntly: "Tutorials interrupt users, don't necessarily improve task performance, and are quickly forgotten." The article recommends help that appears when people need it and is easy to dismiss and find again.

`TourHotspot` is that pattern. It puts a pulsing dot on a corner of its child (or of any `target`) and opens a tip when someone taps or clicks it, as a bottom card below 768px. With `persistKey` the dot disappears for good once the tip has been seen.

```tsx
import { Button } from "@/components/ui/button";
import { TourHotspot } from "@/components/ui/tour";

<TourHotspot
  title="New: sales by state"
  content="Open Reports to see the new chart."
  persistKey="northwind-crm:hint-reports"
  tone="brand"
>
  <Button variant="soft" size="sm">Reports</Button>
</TourHotspot>
```

A rough split that works for us: a hotspot for one new feature, a coachmark tour (`spotlight={false}`, cards with an arrow and no dimming, the page stays usable) for two or three related changes in a release, and the full spotlight tour only for first-run orientation in an app with several areas. A single hint on a control that people already understand belongs in a [Tooltip](https://wingo-ui.com/components/tooltip) or a [Popover](https://wingo-ui.com/components/popover), both free.

## Should I use a tour or an onboarding flow?

They do different jobs. An onboarding flow runs before people reach the app: welcome slides, permission asks, a short profile. The [Onboarding Flow](https://wingo-ui.com/components/onboarding-flow) block covers that, and the [React onboarding flow tutorial](https://wingo-ui.com/blog/react-onboarding-flow) walks through it, including why the notification ask needs a card of its own. A product tour runs inside the app, on the real screens, after that. If you have both, keep the tour to the three or four places people use in their first week.

## What are the common product tour pitfalls?

- **autoStart without persistKey.** The tour opens on every page load. Pair them, or drive `autoStart` from a server flag.
- **Too many steps.** Each extra step is one more chance to press Skip before the part that matters. Split long tours by area and start each one from its page.
- **Steps that describe obvious controls.** A search box does not need a step unless it searches something unexpected.
- **A shortcut that collides.** `?` often opens a shortcuts dialog; the Tour binds nothing until you pass `shortcut`, and our demo uses `shift+h`.
- **A selector that matches a hidden copy.** A string target goes through `document.querySelector`, which returns the first match. If that is the desktop toolbar hidden on a phone, the step shows a centered card. Use a ref or a function that returns the visible element, and `beforeEnter` for anything that takes longer than a second to render.

## How do I add it to my project?

Run `npx wingo-ui@latest add tour`. The CLI copies `tour.tsx` into `components/ui`, adds the Button, Kbd and Progress components and the hooks and helpers it uses, and installs `@radix-ui/react-popover`, `motion` and `lucide-react`. The Tour and the Onboarding Flow are Pro components ([pricing](https://wingo-ui.com/pricing)). More single-component deep dives are in the [component guides category](https://wingo-ui.com/blog/category/components), and the [React form components guide](https://wingo-ui.com/blog/react-form-components-guide) applies the same keyboard, touch and screen reader rules to every form control.

## Components in this post

- [Tour](https://wingo-ui.com/components/tour) (Pro): A React product tour that dims the page around the real UI, explains each step in a card, docks at the bottom on phones and adds hotspots. Install: `npx wingo-ui@latest add tour`
- [Onboarding Flow](https://wingo-ui.com/components/onboarding-flow) (Pro): A React onboarding flow for a marketplace app: welcome slides, notification and location asks, a short profile, preferences and a finish. Install: `npx wingo-ui@latest add onboarding-flow`

## FAQ

### What is the best React tour library?

React Joyride is the most established free option: MIT licensed, with a Joyride component and a useJoyride hook, and its README lists React 16.8 through 19. Driver.js is a small framework-free MIT library, and Shepherd.js needs a commercial license for commercial use. Pick a source component such as the Wingo UI Tour if you want to edit the code and get a phone layout built in.

### How do I show a product tour only once in React?

Store the outcome. With the Wingo UI Tour, persistKey saves completed or skipped in local storage and autoStart leaves a finished tour alone; for signed-in users, also save it on the user record in onComplete and onSkip so a new device does not replay it.

### Should a product tour be skippable?

Yes, on every step. Nielsen Norman Group found that tutorials shown up front interrupt people and are quickly forgotten, so the least you can do is make leaving easy and let people replay the tour later from a Help menu.

### What is the difference between a product tour and a hotspot?

A tour pushes a sequence of steps at people, usually on first run. A hotspot is a small pulsing dot on one element that opens a tip only when someone taps it, so it fits announcing one new feature without interrupting work.

### Is the Wingo UI Tour free?

No. The Tour and the Onboarding Flow block are Pro. The Popover and Tooltip components, which cover single hints without a sequence, are free.

---

Source: https://wingo-ui.com/blog/react-product-tour
