r-tour-guide
v1.0.1
Published
A comprehensive React tour guide library with TypeScript support, works with Next.js
Downloads
282
Maintainers
Readme
React Tour Guide
A comprehensive React tour guide / product-onboarding library with full TypeScript support. Works with Next.js , Vite, Remix, and any React 17+ app.
- 🎯 Precise element targeting — CSS selectors or
data-tour-guideattributes - 🎨 Beautiful, customizable tooltips — colors, labels, and render-props for every part
- 🔄 Real-time position tracking — follows the target during scroll and resize
- 🪄 Fluid movement — optionally eases between steps and stays matched to scroll
- 🧩 Self-contained CSS — no Tailwind or any framework required
- ⚡ SSR-safe — renders nothing on the server, no
window/documentuntil mounted - 🧭 Imperative + declarative — control it via a
refor auto-start - 🔧 Full TypeScript — every prop, step and callback is typed
Installation
npm install r-tour-guideCSS
Import the stylesheet once (it provides the arrows, animations and overlay):
import "r-tour-guide/style.css";Where you import it depends on the framework — see the setup sections below.
Live demo
Quick start
React (Vite / CRA)
import { useRef } from "react";
import {
TourGuideManager,
type TourGuideStep,
type TourGuideManagerRef,
} from "r-tour-guide";
import "r-tour-guide/style.css";
const steps: TourGuideStep[] = [
{
id: "welcome",
title: "Welcome 👋",
content: "This is your first step in the tour.",
target: "welcome", // matches [data-tour-guide="welcome"] or a CSS selector
direction: "bottom",
showAction: true,
},
{
id: "cta",
title: "Get started",
content: "Click here to begin.",
target: "#cta-button",
direction: "top",
showAction: true,
},
];
export default function App() {
const tour = useRef<TourGuideManagerRef>(null);
return (
<div>
<div data-tour-guide="welcome">Welcome card</div>
<button id="cta-button">Get started</button>
<button onClick={() => tour.current?.startTourGuide()}>Start tour</button>
<TourGuideManager ref={tour} steps={steps} />
</div>
);
}Next.js — App Router
TourGuideManager is a client component (it ships with "use client"), so
render it inside a client component. It emits nothing on the server and only
touches the DOM after mount, so it is fully SSR- and static-export-safe.
Import the CSS once in your root layout:
// app/layout.tsx (Server Component — importing CSS here is fine)
import "r-tour-guide/style.css";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}Then use the tour from a client component:
// app/tour.tsx
"use client";
import { useRef } from "react";
import {
TourGuideManager,
type TourGuideStep,
type TourGuideManagerRef,
} from "r-tour-guide";
const steps: TourGuideStep[] = [
{
id: "welcome",
title: "Welcome 👋",
content: "Let's take a quick tour.",
target: "welcome",
showAction: true,
},
];
export function Tour() {
const tour = useRef<TourGuideManagerRef>(null);
return (
<>
<button onClick={() => tour.current?.startTourGuide()}>Start tour</button>
<TourGuideManager ref={tour} steps={steps} autoStart />
</>
);
}// app/page.tsx
import { Tour } from "./tour";
export default function Page() {
return (
<main>
<div data-tour-guide="welcome">Welcome card</div>
<Tour />
</main>
);
}Next.js — Pages Router
Import the CSS in pages/_app.tsx (Next only allows global CSS there):
// pages/_app.tsx
import type { AppProps } from "next/app";
import "r-tour-guide/style.css";
export default function App({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />;
}Then use TourGuideManager in any page/component as usual. No dynamic(() => …,
{ ssr: false }) wrapper is required — the component is already SSR-safe.
API reference
<TourGuideManager> props
| Prop | Type | Default | Description |
| ------------------- | ------------------------------- | ------- | ----------------------------------------------------------- |
| steps | TourGuideStep[] | [] | The tour steps, run in order |
| autoStart | boolean | false | Start automatically on mount |
| showOverlay | boolean | true | Show the dimming overlay around the highlight |
| allowSkip | boolean | true | Show the skip button |
| allowHtml | boolean | false | Render content as HTML (per-step override available) |
| highlightPadding | number | 4 | Padding around the highlighted element (px) |
| labels | TourGuideLabels | — | Global button labels |
| allowInteractions | boolean | false | Allow interacting with the rest of the page during the tour |
| viewportMargin | number | 16 | Minimum distance from the viewport edges |
| scrollToView | boolean | true | Auto-scroll the target into view (per-step override) |
| trackAnimations | boolean | false | Poll target position each frame to follow CSS animations |
| fluid | boolean | false | Ease between steps (matched to scroll); off = instant jump |
| fluidDuration | number | 300 | Fluid transition duration in ms |
| tooltip | TourGuideTooltipCustomization | — | Global tooltip styling (per-step override available) |
Callbacks
| Prop | Type | Description |
| -------------- | ----------------------- | ------------------------------- |
| onStart | () => void | Tour has started |
| onComplete | () => void | Tour finished the last step |
| onSkip | () => void | Tour was skipped |
| onStepChange | (step, index) => void | Fired whenever the step changes |
Render-props — each replaces part of the tooltip (the React equivalent of slots):
renderContent, renderHeader, renderSkipButton, renderProgress,
renderActions. See Render-props.
Imperative ref — TourGuideManagerRef
const tour = useRef<TourGuideManagerRef>(null);
tour.current?.startTourGuide();
tour.current?.nextStep();
tour.current?.previousStep();
tour.current?.goToStep(2);
tour.current?.skipTourGuide();
tour.current?.completeTourGuide();
tour.current?.isActive; // boolean
tour.current?.currentStepIndex; // numberTourGuideStep
interface TourGuideStep {
id: string; // Unique step id
title: string; // Tooltip title
content?: string; // Tooltip body
allowHtml?: boolean; // Render content as HTML (overrides the manager prop)
target: string; // CSS selector or data-tour-guide value to highlight
tooltipTarget?: string; // Separate element to position the tooltip against
direction?: "top" | "bottom" | "left" | "right";
offsetX?: number; // Fine-tune tooltip x (px)
offsetY?: number; // Fine-tune tooltip y (px)
radius?: number; // Highlight cut-out radius (px, default 8)
scrollToView?: boolean; // Per-step auto-scroll (overrides the manager prop)
showAction?: boolean; // Show the prev/next buttons
skipLabel?: string;
nextLabel?: string;
prevLabel?: string;
finishLabel?: string;
tooltip?: TourGuideTooltipCustomization; // Per-step styling
beforeShow?: () => void | Promise<void>;
afterShow?: () => void;
beforeHide?: () => void | Promise<void>;
}TourGuideLabels
interface TourGuideLabels {
skip?: string;
next?: string;
previous?: string;
finish?: string;
}TourGuideTooltipCustomization
interface TourGuideTooltipCustomization {
backgroundColor?: string; // solid color OR a CSS gradient
textColor?: string;
borderRadius?: string;
padding?: string;
maxWidth?: string;
minWidth?: string;
boxShadow?: string;
buttonBackgroundColor?: string;
buttonTextColor?: string;
buttonHoverColor?: string;
skipButtonColor?: string;
skipButtonHoverColor?: string;
progressActiveColor?: string;
progressInactiveColor?: string;
tooltipClassName?: string;
headerClassName?: string;
contentClassName?: string;
actionsClassName?: string;
}Element targeting
target (and tooltipTarget) resolve in two ways:
- As a CSS selector —
#id,.class,[data-x], etc. - As a
data-tour-guidevalue —target: "welcome"matches<div data-tour-guide="welcome">.
<div data-tour-guide="welcome-card">…</div> // target: "welcome-card"
<button id="cta">Go</button> // target: "#cta"Tooltip customization
Style all tooltips globally, or override per step (per-step wins):
<TourGuideManager
steps={steps}
tooltip={{
backgroundColor: "#0f172a",
textColor: "#fff",
progressActiveColor: "#60a5fa",
}}
/>const steps = [
{
id: "custom",
title: "Fancy",
content: "Gradient tooltip",
target: "card",
tooltip: {
backgroundColor: "linear-gradient(135deg, #6366f1, #d946ef)",
textColor: "#fff",
},
},
];Render-props
Replace any part of the tooltip with your own JSX:
<TourGuideManager
steps={steps}
renderContent={({ step }) => (
<div>
<p>{step?.content}</p>
<a href="/docs">Read the docs →</a>
</div>
)}
renderActions={({ isLastStep, showPrevious, onPrevious, onNext }) => (
<>
{showPrevious && <button onClick={onPrevious}>Back</button>}
<button onClick={onNext}>{isLastStep ? "Done" : "Next"}</button>
</>
)}
/>Available: renderHeader, renderContent, renderSkipButton,
renderProgress, renderActions.
Advanced
Separate tooltip positioning
Highlight one element while pointing the tooltip at another:
{
id: "advanced",
title: "Heads up",
content: "Highlights the checkbox, tooltip points at the save button.",
target: "notification-checkbox",
tooltipTarget: "#save-settings",
direction: "top",
}Lifecycle hooks
{
id: "welcome",
title: "Welcome",
target: "card",
beforeShow: async () => { await loadData(); },
afterShow: () => track("tour_step_shown"),
beforeHide: async () => { /* cleanup */ },
}Fluid movement
fluid eases the highlight and tooltip between steps and while the target
moves, and suppresses the transition during active scrolling so it stays
matched to the page. Tune the speed with fluidDuration (ms):
<TourGuideManager steps={steps} fluid fluidDuration={500} />Following animated targets
If a target moves under a CSS animation while its step is showing, enable
trackAnimations (it polls each frame — off by default because it forces a
layout per frame):
<TourGuideManager steps={steps} trackAnimations />Rich content
- Line breaks work in a plain
contentstring — newlines are preserved. - HTML via
allowHtml(per-step or on the manager). Not sanitized — only use it for content you control. - Anything interactive via the
renderContentrender-prop.
{ id: "a", title: "Tips", target: "x", content: "Line one.\nLine two." }
{ id: "b", title: "Shortcut", target: "y", allowHtml: true,
content: "Press <kbd>Cmd</kbd>+<kbd>K</kbd>." }useTourGuide()
Shared, persisted tour state (localStorage-backed, SSR-safe). Any component can read it:
const { tourGuideState, resetTourGuide, isStepCompleted } = useTourGuide();
tourGuideState.isActive;
tourGuideState.completedSteps;
tourGuideState.hasSeenTourGuide;Accessibility
- The highlighted, interactive element gets a visible focus outline.
- Honors
prefers-reduced-motion(disables fluid transitions) andprefers-contrast. - Tour elements are hidden in print.
Next.js / SSR notes
TourGuideManagerrendersnullon the server and until it mounts on the client, so hydration is safe.- No
window/documentaccess happens during render. - No
dynamic(..., { ssr: false })wrapper needed.
Examples
See examples/basic-usage.tsx for a complete,
copy-pasteable example, and example/ for the full demo site.
License
MIT
