A lightweight, animated product tour and onboarding library for React. Built with Framer Motion for buttery-smooth transitions, smart positioning, and a stellar developer experience.
- Smooth Animations: Powered by
framer-motionwith spring physics. - Smart Auto-Positioning: Tooltip dynamically recalculates coordinates, bounds, and automatically flips if it touches viewport edges.
- Lazy Loading Support: Seamlessly handles elements that are rendered asynchronously or lazy-loaded via
MutationObserverwithout manual re-triggers. - Headless Override: Render your completely custom React components inside the tooltip while keeping the smart positioning.
- Keyboard Navigation: Navigate seamlessly with
Arrow Keys,Enterand close withEscape. - Zero-CSS Import: Styles are auto-injected. Just override CSS variables to theme it!
- Cross-Page Tours: Continues the tour even when the route changes.
npm install modern-tour(Note: framer-motion and lucide-react are peer/internal dependencies, make sure you have react installed)
- Wrap your application with the
<TourProvider>and define your steps. - Use the
useTourhook anywhere inside to control the tour.
import { TourProvider, useTour } from 'modern-tour';
const steps = [
{ target: '#btn-1', content: 'Welcome to our platform!' },
{ target: '#btn-2', title: 'Settings', content: 'Configure your preferences here.' },
];
function YourApp() {
const { start } = useTour();
return (
<div>
<button id="btn-1" onClick={() => start()}>Start Tour</button>
<button id="btn-2">Settings</button>
</div>
);
}
function Root() {
return (
<TourProvider options={{ steps, animation: 'smooth' }}>
<YourApp />
</TourProvider>
);
}Modern Tour uses pure CSS variables for theming. You don't need to import any CSS files. Just override the variables in your global CSS.
:root {
/* Default Minimal Style */
--tour-bg: #ffffff;
--tour-text: #09090b;
--tour-text-secondary: #71717a;
--tour-primary: #18181b;
--tour-primary-foreground: #fafafa;
--tour-border: #e4e4e7;
--tour-radius: 0.5rem;
--tour-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1);
}
/* Dark Mode Example */
.dark {
--tour-bg: #09090b;
--tour-text: #fafafa;
--tour-primary: #fafafa;
--tour-primary-foreground: #18181b;
--tour-border: #27272a;
}
/* Neo-Brutalism Example */
.neo-theme {
--tour-bg: #fffbf0;
--tour-text: #000;
--tour-border: #000;
--tour-border-width: 3px;
--tour-radius: 0px;
--tour-shadow: 6px 6px 0px #000000;
}If tweaking CSS variables isn't enough, you can entirely override the content of the tooltip with your own React component.
import { TourProvider, useTour } from 'modern-tour';
function MyCustomTooltip() {
// Access tour state and controls natively
const { step, currentStep, totalSteps, next, prev, stop } = useTour();
return (
<div className="custom-tooltip bg-black text-white p-4 rounded-xl">
<h2>{step?.title}</h2>
<p>{step?.content}</p>
<div className="flex justify-between mt-4">
<span>{currentStep + 1} / {totalSteps}</span>
<div>
<button onClick={prev}>Back</button>
<button onClick={next}>Next</button>
</div>
</div>
</div>
);
}
// In your root:
<TourProvider
options={{
steps,
components: { TooltipContent: MyCustomTooltip }
}}
>
<App />
</TourProvider>If your target element is rendered lazily (e.g., inside a tab or loaded via network), Modern Tour uses MutationObserver to instantly catch it when it mounts.
If your component takes longer than 3 seconds to load, increase the timeout:
<TourProvider options={{
steps,
waitForTargetTimeout: 10000 // Waits up to 10 seconds before aborting
}}>To create a tour that spans multiple routes/pages, assign a route to your step and handle the navigation using onStepChange:
<TourProvider options={{
steps: [
{ target: '#home-btn', content: 'We are on Home' },
{ target: '#settings-btn', content: 'We are on Settings', route: '/settings' }
],
onStepChange: (stepIndex) => {
const nextRoute = steps[stepIndex]?.route;
if (nextRoute) {
navigate(nextRoute); // Using React Router or Next.js router
}
}
}}>| Property | Type | Default | Description |
|---|---|---|---|
steps |
TourStep[] |
[] |
Array of steps defining your tour. |
autoStart |
boolean |
false |
Start tour automatically when provider mounts. |
animation |
string / Config |
'smooth' |
Preset (fade, scale, slide, bounce, smooth) or custom Framer Motion config. |
components |
Object |
undefined |
Custom component overrides (e.g. { TooltipContent: MyReactComponent }). |
waitForTargetTimeout |
number |
3000 |
Max milliseconds to wait for a lazy-loaded target element. |
keyboardNavigation |
boolean |
true |
Allow ArrowLeft, ArrowRight, Enter, and Escape controls. |
closeOnOverlayClick |
boolean |
true |
Close tour when user clicks the dark background. |
spotlightPadding |
number |
8 |
Pixels of padding around the highlighted target element. |
labels |
Object |
{...} |
Override default text for buttons (next, prev, skip, finish, close). |
| Property | Type | Description |
|---|---|---|
target |
string |
Required. CSS Selector for the element to highlight (e.g., #my-btn, .nav-item). |
content |
ReactNode |
Required. Body content of the step. |
title |
ReactNode |
Optional heading text. |
position |
string |
Preferred placement (top, bottom, left, right and -start/-end variations). Defaults to bottom. |
route |
string |
Helpful meta-field to trigger route changes on specific steps. |
spotlightPadding |
number |
Step-specific padding, overrides the global setting. |
onActive |
() => void |
Callback triggered exactly when this step becomes visible. |
onLeave |
() => void |
Callback triggered when leaving this step. |
Contributions, issues and feature requests are welcome! Feel free to check issues page.
This project is MIT licensed.
