Zero-dependency React onboarding tour with SVG spotlight, smart tooltip placement, and keyboard navigation.
- No external dependencies — built entirely with React + TypeScript
- SVG spotlight — smooth cutout effect that works with any layout (no z-index hacks)
- Smart placement — tooltip auto-positions to avoid viewport edges
- Keyboard navigation —
→next,←back,Escapeto skip - Next.js App Router ready — all client components marked with
"use client" - Fully typed — complete TypeScript types included
npm install react-spotlight-onboard
# or
pnpm add react-spotlight-onboard
# or
yarn add react-spotlight-onboard// app/layout.tsx or a providers file ("use client" required)
import { TourProvider } from "react-spotlight-onboard";
export function Providers({ children }: { children: React.ReactNode }) {
return (
<TourProvider>
{children}
</TourProvider>
);
}<nav data-tour-id="sidebar-nav">...</nav>
<header data-tour-id="main-header">...</header>
<div data-tour-id="dashboard-stats">...</div>"use client";
import { useOnboardingTour } from "react-spotlight-onboard";
import { useEffect } from "react";
export function DashboardPage() {
const { startTour } = useOnboardingTour();
useEffect(() => {
startTour(
"dashboard",
[
{
id: "step-1",
target: "sidebar-nav",
title: "Navigation",
description: "Use the sidebar to move between sections.",
},
{
id: "step-2",
target: "main-header",
title: "Header",
description: "Your account and settings are up here.",
placement: "bottom",
},
{
id: "step-3",
target: "dashboard-stats",
title: "Dashboard",
description: "Your key metrics at a glance.",
placement: "top",
},
],
{
onComplete: (tourId) => {
localStorage.setItem(`tour-${tourId}-done`, "true");
},
onSkip: (tourId, stepIndex) => {
console.log(`Skipped tour "${tourId}" at step ${stepIndex}`);
},
}
);
}, []);
return <main>...</main>;
}Wrap your app once. Renders the spotlight overlay automatically when a tour is active.
| Prop | Type | Default | Description |
|---|---|---|---|
zIndex |
number |
9000 |
z-index of the overlay stack |
backdropOpacity |
number |
0.6 |
Darkness of the backdrop (0–1) |
const {
startTour, // (tourId, steps, callbacks?) => void
stopTour, // () => void — stops without firing onSkip
nextStep, // () => void
prevStep, // () => void
skipTour, // () => void — fires onSkip callback
isActive, // boolean
activeTourId, // string | null
currentStep, // TourStep | null
currentStepIndex, // number
totalSteps, // number
} = useOnboardingTour();interface TourStep {
id: string;
target: string; // matches data-tour-id="..." on a DOM element
title: string;
description: string;
placement?: "top" | "bottom" | "left" | "right" | "auto"; // default: "auto"
spotlightPadding?: number; // px padding around the spotlight cutout; default: 8
customContent?: ReactNode; // rendered below the description
}startTour("my-tour", steps, {
onComplete: (tourId: string) => void,
onSkip: (tourId: string, stepIndex: number) => void,
onStepChange: (tourId: string, step: TourStep, index: number) => void,
});| Key | Action |
|---|---|
→ ArrowRight |
Next step |
← ArrowLeft |
Previous step |
Escape |
Skip tour |
The spotlight effect uses an SVG mask that cuts a rounded rectangle hole over the target element. This approach is immune to CSS stacking context issues that affect clip-path-based approaches — it works correctly inside sidebars, modals, and transformed containers.
Target elements are resolved via document.querySelector('[data-tour-id="..."]'). The tooltip placement algorithm measures available viewport space in all four directions and picks the side with the most room, clamping the position to always stay within the viewport.
All interactive components include the "use client" directive. Place TourProvider inside a Client Component boundary:
// providers.tsx
"use client";
import { TourProvider } from "react-spotlight-onboard";
export function Providers({ children }: { children: React.ReactNode }) {
return <TourProvider>{children}</TourProvider>;
}// app/layout.tsx (Server Component)
import { Providers } from "./providers";
export default function RootLayout({ children }) {
return (
<html>
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}MIT