@oqlet/react-driver
v1.0.2
Published
Schema-driven, plug-and-play guided tours for React — built on driver.js.
Maintainers
Readme
ReactDriver
A schema-driven guided tour library for React. Define product onboarding, feature walkthroughs, cross-page tours, beacons, tooltips, and analytics as typed configuration instead of wiring tour state by hand.
Built on driver.js for the spotlight, overlay, and popover engine. ReactDriver adds React hooks, JSX content, route-aware step lifecycle hooks, persistence, analytics adapters, tour registration, and testing utilities.
Features
- Schema-driven tours - define every step as a plain
TourStep[] - React-first API -
useTour,TourProvider, named tour controls, and composable hooks - JSX content - render strings, rich React nodes, custom popovers, images, buttons, or design-system components
- Cross-route navigation -
beforeNext,beforePrev,afterNext,afterPrev, andwaitForTargetsupport page transitions - Action-driven steps - advance from clicks with
advanceOn, validation withcanAdvance, timers withautoAdvanceAfter - Persistence - remember completed tours, resume progress, cap show count, or re-show when
versionchanges - Scheduling - delay tours by time, page visits, release date, or idle time
- Responsive steps - override copy, placement, and behavior on mobile with
mobileOverrides - Conditional visibility - skip steps at runtime with
visibleWhen - Tour registry - register tours globally and start them from any component without prop drilling
- Sequences - play multiple named tours back-to-back with
useTourSequence - Beacons and tooltips - ship non-blocking feature discovery alongside full tours
- Analytics - lifecycle hooks plus PostHog, Segment, Mixpanel, and Amplitude adapters
- Testing utilities - mock
TourControlscleanly in Vitest/Jest - TypeScript-first - typed config, controls, render props, analytics metadata, and utility exports
Installation
npm install @oqlet/react-driver
# peer dependencies, if not already installed
npm install react react-domdriver.js is installed as a package dependency. Import the CSS once in your app entry:
import "@oqlet/react-driver/driver.css";Compatibility - peer dependencies support React 17+. The library is tested in this repo with React 19.
Bundling - the package ships ESM, CJS, UMD, CSS, and TypeScript declarations.
Quick Start
import { useTour, type TourStep } from "@oqlet/react-driver";
import "@oqlet/react-driver/driver.css";
const steps: TourStep[] = [
{
target: "#workspace-switcher",
title: "Switch workspaces",
content: "Jump between teams, clients, or environments from here.",
side: "bottom",
},
{
target: "#create-project",
title: "Create a project",
content: "Start a new project with the defaults your team already uses.",
},
{
target: "#invite-team",
title: "Invite your team",
content: "Bring collaborators in when the setup is ready.",
},
];
export default function Dashboard() {
const tour = useTour({
id: "dashboard-onboarding",
steps,
persist: true,
showProgress: true,
onFinish: () => console.log("Tour completed"),
});
return (
<button onClick={() => tour.start()}>
Take the tour
</button>
);
}TourStep Props
| Prop | Type | Description |
|------|------|-------------|
| target | string \| RefObject<Element \| null> | Element to highlight. Omit for a centered popover. |
| title | ReactNode | Popover heading. |
| content | ReactNode | Required popover body. Strings and JSX are both supported. |
| side | "top" \| "bottom" \| "left" \| "right" | Popover side. |
| align | "start" \| "center" \| "end" | Popover alignment. |
| popoverClass | string | Extra class for this step's popover. |
| popoverless | boolean | Highlight the target without rendering a popover. |
| section | string | Group label used by TourChecklist. |
| mobileOverrides | Partial<TourStep> | Step overrides below the configured breakpoint. |
| visibleWhen | () => boolean | Skip this step when it returns false. |
| delayBefore | number | Wait before showing the popover after highlight. |
| delayAfter | number | Wait before moving to the next/previous step. |
| highlightPadding | number | Per-step overlay cutout padding. |
| canAdvance | () => boolean \| Promise<boolean> | Block Next until the guard passes. |
| autoAdvanceAfter | number | Automatically continue after N milliseconds. |
| advanceOn | string | Click selector that advances the current step. |
| beforeNext / beforePrev | () => void \| Promise<void> | Run before navigation. Good for route changes. |
| afterNext / afterPrev | () => void | Run after the next/previous step has settled. |
| onBeforeHighlight | () => void | Called before the step is highlighted. |
| onAfterHighlight | () => void | Called after the step is highlighted. |
| onDeselected | () => void | Called when leaving the step. |
useTour Config
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| steps | TourStep[] | required | Step definitions. |
| id | string | - | Required for persistence, registry, and history helpers. |
| persist | boolean \| "session" | false | Store completion in localStorage or sessionStorage. |
| persistProgress | boolean | false | Resume from the last reached step. |
| version | string | - | Re-show a persisted tour when the version changes. |
| showCount | number | - | Show at most N times. |
| showAfter | { delay?: number; visits?: number; date?: string \| Date } | - | Schedule automatic start. |
| stepsUrl | string | - | Fetch steps at runtime. |
| stepsTransform | (data: unknown) => TourStep[] | identity | Map remote data into steps. |
| breakpoint | number | 768 | Width below which mobileOverrides apply. |
| highlightPadding | number | driver.js default | Extra overlay cutout padding. |
| keyboard | { enabled?: boolean; next?: string; prev?: string; close?: string } | enabled | Configure keyboard navigation. |
| waitForIdle | boolean \| number | false | Start after browser idle time. |
| waitForTarget | number | 5000 | Time to wait for route-created targets. |
| scrollBehavior | "smooth" \| "instant" \| false | "smooth" | Target scrolling behavior. |
| showProgress | boolean | true | Show step count in the popover. |
| animate | boolean | true | Animate step transitions. |
| overlayOpacity | number | 0.75 | Overlay darkness from 0 to 1. |
| allowClose | boolean | true | Allow Escape/overlay close. |
| overlayClass | string | - | Extra overlay class. |
| popoverClass | string | - | Extra class for all popovers. |
| prevBtnText / nextBtnText / doneBtnText | string | built-in labels | Button labels. |
| renderPopover | FC<PopoverRenderProps> | - | Replace the default popover. |
| onBeforeStart | () => boolean \| Promise<boolean> | - | Abort start by returning false. |
| onError | (error: Error, context: string) => void | - | Capture recoverable errors. |
| debug | boolean | false | Log step transitions and visibility decisions. |
| onStart / onFinish / onSkip | () => void | - | Tour lifecycle callbacks. |
| onStepChange | (stepIndex: number) => void | - | Called on current step change. |
| onStepEnter | (index, meta) => void | - | Analytics hook when a step becomes visible. |
| onStepExit | (index, meta) => void | - | Analytics hook when leaving a step. |
Tour Controls
useTour and useRegisterTour return a TourControls object:
| Control | Type | Description |
|---------|------|-------------|
| start | (stepIndex?: number) => void | Start the tour, optionally from a specific step. |
| stop | () => void | Stop and destroy the active tour. |
| restart | () => void | Restart from step 0 without firing finish/skip. |
| next | () => void | Advance one step. |
| prev | () => void | Move back one step. |
| moveTo | (stepIndex: number) => void | Jump to a step by index. |
| onComplete | (cb) => () => void | Subscribe to finish/skip from outside the config. |
| isActive | boolean | True while the tour is running. |
| currentStep | number | Current visible step index. |
| totalSteps | number | Number of visible steps. |
Cross-Route Tours
Use beforeNext or beforePrev to navigate, then let ReactDriver wait for the next target.
import { useNavigate } from "react-router-dom";
import { useTour } from "@oqlet/react-driver";
function ProductTour() {
const navigate = useNavigate();
const tour = useTour({
id: "checkout",
waitForTarget: 7000,
steps: [
{
target: "#product-card",
title: "Choose a product",
content: "Start from any product card in the catalog.",
},
{
target: "#cart-summary",
title: "Review your cart",
content: "The tour waits until the cart route has rendered.",
beforeNext: async () => navigate("/cart"),
},
{
target: "#checkout-button",
title: "Checkout",
content: "Finish the purchase flow here.",
},
],
});
return <button onClick={() => tour.start()}>Start checkout tour</button>;
}Action-Driven Steps
Move the tour forward only after the user performs the right action.
const tour = useTour({
id: "profile-setup",
steps: [
{
target: "#profile-menu",
title: "Open profile settings",
content: "Click this menu item to continue.",
advanceOn: "#profile-menu",
beforeNext: () => setProfilePanelOpen(true),
afterNext: () => setMenuOpen(false),
},
{
target: "#display-name",
title: "Add your display name",
content: "Enter at least 3 characters before continuing.",
canAdvance: () => displayName.trim().length >= 3,
},
],
});Persistence and Scheduling
useTour({
id: "new-dashboard",
version: "2.0",
persist: true,
persistProgress: true,
showCount: 3,
showAfter: {
visits: 2,
delay: 3000,
},
steps,
});| Option | What it does |
|--------|--------------|
| persist: true | Do not show again after completion. |
| persist: "session" | Remember completion only for the current tab session. |
| persistProgress: true | Resume from the last reached step. |
| version | Invalidate old completion state when the tour changes. |
| showCount | Stop auto-showing after N starts. |
| showAfter.delay | Auto-start after N milliseconds. |
| showAfter.visits | Auto-start from the Nth visit onward. |
| showAfter.date | Auto-start only after a date. |
Global Tours
Wrap your app with TourProvider, register tours near the UI they describe, and start them from anywhere.
import {
TourProvider,
useRegisterTour,
useTourControls,
} from "@oqlet/react-driver";
function App() {
return (
<TourProvider>
<Dashboard />
<HelpMenu />
</TourProvider>
);
}
function Dashboard() {
useRegisterTour("dashboard", {
persist: true,
steps: [
{ target: "#stats", title: "Stats", content: "Your key metrics live here." },
{ target: "#filters", title: "Filters", content: "Narrow down the view." },
],
});
return <main>...</main>;
}
function HelpMenu() {
const dashboardTour = useTourControls("dashboard");
return <button onClick={() => dashboardTour?.start()}>Replay dashboard tour</button>;
}Tour Sequences
Chain registered tours into a single onboarding flow.
function OnboardingButton() {
const sequence = useTourSequence(["setup", "dashboard", "billing"]);
return (
<button onClick={sequence.startSequence}>
Start onboarding
</button>
);
}Beacons, Checklists, and Tooltips
import {
TourBeacon,
TourChecklist,
TourTooltip,
useRegisterTour,
} from "@oqlet/react-driver";
const controls = useRegisterTour("reports", {
steps: [
{ section: "Reports", target: "#report-type", title: "Report type", content: "Choose the format." },
{ section: "Reports", target: "#date-range", title: "Date range", content: "Pick the reporting period." },
],
});
<TourBeacon target="#reports-nav" tourId="reports" position="bottom-right" />;
<TourChecklist
steps={steps}
currentStep={controls.currentStep}
isActive={controls.isActive}
onJumpTo={controls.moveTo}
/>;
<TourTooltip
target="#export-button"
title="Export"
content="Download this report as CSV."
trigger="hover"
/>;Custom Popovers
Use renderPopover when your app needs a fully custom design-system surface.
useTour({
steps,
renderPopover: ({ step, stepIndex, totalSteps, next, prev, stop, isFirst, isLast }) => (
<section className="rounded-lg border bg-white p-4 shadow-xl">
<h2>{step.title}</h2>
<div>{step.content}</div>
<footer>
<span>{stepIndex + 1} / {totalSteps}</span>
<button onClick={stop}>Close</button>
<button onClick={prev} disabled={isFirst}>Back</button>
<button onClick={next}>{isLast ? "Done" : "Next"}</button>
</footer>
</section>
),
});Remote Steps
Load copy and selectors from an endpoint when product or growth teams need to update tours without redeploying the app.
useTour({
id: "release-tour",
steps: [],
stepsUrl: "/api/tours/release-tour",
stepsTransform: (data) => {
const payload = data as {
steps: Array<{ selector: string; heading: string; body: string }>;
};
return payload.steps.map((step) => ({
target: step.selector,
title: step.heading,
content: step.body,
}));
},
});Analytics
Use lifecycle callbacks directly or spread in a prebuilt adapter.
import posthog from "posthog-js";
import { adapters, useTour } from "@oqlet/react-driver";
useTour({
...adapters.posthog(posthog, { tourId: "activation" }),
steps,
});Custom analytics:
useTour({
steps,
onStart: () => analytics.track("Tour Started"),
onFinish: () => analytics.track("Tour Completed"),
onSkip: () => analytics.track("Tour Skipped"),
onStepEnter: (index, { step }) => {
analytics.track("Tour Step Viewed", {
index,
title: typeof step.title === "string" ? step.title : undefined,
});
},
onStepExit: (index, { duration, reason }) => {
analytics.track("Tour Step Exited", { index, duration, reason });
},
});Built-in adapters:
| Adapter | Import |
|---------|--------|
| PostHog | adapters.posthog(posthog, { tourId }) |
| Segment | adapters.segment(window.analytics, { tourId }) |
| Mixpanel | adapters.mixpanel(mixpanel, { tourId }) |
| Amplitude | adapters.amplitude(amplitude, { tourId }) |
Localization
locales contains button-label presets for en, fr, es, de, pt, it, nl, ja, zh, ko, ar, ru, and hi.
import { locales, useTour } from "@oqlet/react-driver";
useTour({
...locales.fr,
steps,
});You can also pass labels directly:
useTour({
prevBtnText: "Previous",
nextBtnText: "Continue",
doneBtnText: "Finish",
steps,
});Testing
Mock tour controls without rendering driver.js in unit tests.
import { createMockTour } from "@oqlet/react-driver/testing";
import { vi, expect } from "vitest";
const mockTour = createMockTour({
isActive: true,
currentStep: 1,
});
vi.mock("@oqlet/react-driver", () => ({
useTour: () => mockTour,
}));
expect(mockTour.start).toHaveBeenCalledWith(0);CDN / UMD
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@oqlet/react-driver/dist/driver.css"
/>
<script src="https://unpkg.com/react/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom/umd/react-dom.production.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@oqlet/react-driver/dist/react-driver.umd.js"></script>
<script>
const { useTour } = ReactDriver;
</script>API Exports
import {
TourProvider,
TourBeacon,
TourChecklist,
TourTooltip,
useTour,
useRegisterTour,
useTourControls,
useIsTourActive,
useTourStep,
useTourSequence,
useStepRef,
useTourHistory,
useTourAnalytics,
waitForElement,
hasSeenTour,
skipTour,
clearTourHistory,
locales,
adapters,
} from "@oqlet/react-driver";
import type {
TourStep,
TourConfig,
TourControls,
PopoverRenderProps,
StepExitReason,
} from "@oqlet/react-driver";Testing export:
import { createMockTour } from "@oqlet/react-driver/testing";CSS export:
import "@oqlet/react-driver/driver.css";Contributing
git clone https://github.com/goutham-05/react-driver
cd react-driver
npm install
npm test # vitest - 128 tests
npm run build # tsup - ESM + CJS + .d.cts + CSS + UMD
cd website
npm install
npm run build # Vite websiteLicense
MIT
