Guide.js
All posts

April 10, 2026 · Mukesh

How to build a product tour in React

Ship a working four-step React product tour in under an hour. No backend, no provider wrapping, no global store.

Most React product tour tutorials bury you in setup. Provider wrappers, reducer state, stepIndex wiring, callback plumbing. You’re two hundred lines in before the first tooltip shows up.

It doesn’t have to be like that.

This post ships a working four-step tour in a React app in under an hour. No backend, no provider, no global store. The tour library is Guide.js, a single stateless component. Use something else if you want. The structure is the same.

What you need

A React app. Node 18+. A few elements on a page you want to anchor tooltips to. That’s it.

Install

Terminal window
npm install guide.js

The library ships ESM and CJS. Works with Vite, Next.js, Remix, CRA, any React setup.

Import the stylesheet once in your entry file.

import "guide.js/styles.css";

Define your steps

A tour is a list of steps. Each step has an id, some content, and a configuration.

const steps = [
{
id: "welcome",
title: "Welcome to the app",
description: "Let us show you around in thirty seconds.",
configuration: {
type: "dialog",
progressButtonText: "Show me",
isClosable: true,
},
},
{
id: "new-project",
title: "Start a new project here",
description: "Click the plus icon any time.",
configuration: {
type: "pointer",
customPivotSelector: "[data-tour='new-project']",
progressButtonText: "Next",
},
},
{
id: "sidebar",
title: "Your projects live here",
description: "Pinned ones stay at the top.",
configuration: {
type: "pointer",
customPivotSelector: "[data-tour='sidebar']",
progressButtonText: "Got it",
},
},
{
id: "done",
title: "You're set",
description: "Hit ⌘K any time to reopen this tour.",
configuration: {
type: "dialog",
progressButtonText: "Start working",
},
},
];

Two step types cover most tours. Dialog for framing. Pointer for anchoring to specific UI.

Render the Guide

import { useState } from "react";
import { Guide } from "guide.js";
export function App() {
const [isActive, setIsActive] = useState(true);
return (
<>
<YourActualApp />
<Guide
tour={{ id: "onboarding", title: "Onboarding", steps }}
steps={steps}
isActive={isActive}
onComplete={() => setIsActive(false)}
onClose={() => setIsActive(false)}
/>
</>
);
}

That’s the whole API. You control when the tour runs. No provider wrapping your tree, no reducer, no context.

Anchoring to elements

The customPivotSelector on each step is any CSS selector. Class names, IDs, data-attributes, all fine.

Use data-attributes. They survive refactors.

<button data-tour="new-project">New project</button>

If you swap the class name or rewrite the button, the selector still matches. Tours built on class names break silently six months later.

Showing the tour only to new users

Store a flag in localStorage and check it on mount.

import { useEffect, useState } from "react";
export function App() {
const [isActive, setIsActive] = useState(false);
useEffect(() => {
if (!localStorage.getItem("seen-onboarding")) {
setIsActive(true);
localStorage.setItem("seen-onboarding", "1");
}
}, []);
return (
<Guide
tour={{ id: "onboarding", title: "Onboarding", steps }}
steps={steps}
isActive={isActive}
onComplete={() => setIsActive(false)}
/>
);
}

For production, use a server-side check keyed off the user, not the browser. localStorage is fine for a prototype.

Handling page navigation mid-tour

If a step lives on a different route, navigate first, then advance. Lifecycle callbacks give you the hooks.

<Guide
// ...
onStepChange={(step) => {
if (step.id === "billing" && window.location.pathname !== "/settings") {
navigate("/settings");
}
}}
/>

Wait one frame after navigation so the target element mounts. Most routers flush synchronously. Effects that depend on new data will not.

What this won’t do

Tours you design, then forget, rot in production. Users hit them a year later, selectors have moved, and every step breaks. A stateless component won’t save you from that. Set up a smoke test for the tour or it will break.

If you want analytics on step completion, copy edits by a non-engineer, and per-audience targeting, reach for a hosted tour product. Guide.js stays out of that. It’s the component. You own the plumbing.

For a first-pass, in-app, engineer-owned product tour, this is under an hour. Most of the work is writing the copy.