A component to make product tours with motion and shadcn/ui.
TourProvider: A context provider to make the tour available to the app.TourAlertDialog: An alert dialog component to begin the tour or skip it.useTour: A hook about the tour which provides necessary functions and states.
- First install the packages and necessary components by running the following command:
pnpx shadcn add https://tour.niazmorshed.dev/tour.jsonIt will add TourProvider, TourAlertDialog and useTour hook in your project under components/tour.tsx.
Additionally, a sets of step ids will get added under lib/tour-constants.ts.
Note:
TOUR_STEPSis the name of the constant that contains the step ids. A step id should be assigned to a selector that is used to highlight the step. It will calculate its position in the viewport and show the step beside it.
- Next wrap your app with
TourProviderinapp/layout.tsxor somewhere else you wish.
import { TourProvider } from "@/components/tour";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return <TourProvider>{children}</TourProvider>;
}- Next use the
setStepsfromuseTourhook to set the steps you want to highlight. And use theTourAlertDialogcomponent to show the tour.
const steps: TourStep[] = [
{
content: <div>Team Switcher</div>,
selectorId: TOUR_STEP_IDS.TEAM_SWITCHER,
position: "right",
onClickWithinArea: () => { },
},
{
content: <div>Writing Area</div>,
selectorId: TOUR_STEP_IDS.WRITING_AREA,
position: "left",
onClickWithinArea: () => { },
},
];
function Page() {
const { setSteps } = useTour();
const [openTour, setOpenTour] = useState(false);
useEffect(() => {
setSteps(steps);
const timer = setTimeout(() => {
setOpenTour(true);
}, 100);
return () => clearTimeout(timer);
}, [setSteps]);
return <div>
<TourAlertDialog open={openTour} onOpenChange={setOpenTour} />
</div>;
}- Remember to add the step ids to the selectors you want to highlight. Here is an example of how to do it:
<div id={TOUR_STEP_IDS.TEAM_SWITCHER}>Team Switcher</div>You can register multiple tours and start them by ID using the tours prop on TourProvider:
import { TourProvider, TourDefinition } from "@/components/tour";
const tours: TourDefinition[] = [
{ id: "intro", steps: introSteps },
{ id: "advanced", steps: advancedSteps },
];
<TourProvider
tours={tours}
onComplete={(tourId) => console.log(`${tourId} completed`)}
>
{children}
</TourProvider>Then start a specific tour by ID:
const { startTour } = useTour();
<button onClick={() => startTour("intro")}>Start Intro Tour</button>
<button onClick={() => startTour("advanced")}>Start Advanced Tour</button>Note: The original
setSteps+startTour()(no arguments) pattern continues to work for single-tour usage.
When a tour is active, users can navigate using the keyboard:
- ArrowRight — advance to the next step
- ArrowLeft — go back to the previous step
- Escape — skip/dismiss the tour
The spotlight cutout supports configurable padding and border-radius per step:
const steps: TourStep[] = [
{
content: <div>This element has a rounded spotlight</div>,
selectorId: "my-element",
padding: 12, // spotlight padding around element (default: 8)
borderRadius: 16, // spotlight corner radius (default: 8)
},
];A "Skip tour" text button appears on every step by default. It is automatically hidden on the last step (where "Finish" is shown instead). You can also hide it on specific steps:
const steps: TourStep[] = [
{
content: <div>This step has no skip button</div>,
selectorId: "important-step",
showSkip: false,
},
];An optional X icon button can be shown in the top-right of each tooltip. Set closeable on TourProvider to enable it for all steps, or on individual steps to override:
// Enable for all steps in the tour
<TourProvider closeable>
{children}
</TourProvider>
// Or override per step
const steps: TourStep[] = [
{
content: <div>This step has a close button</div>,
selectorId: "my-element",
closeable: true,
},
{
content: <div>This step does not</div>,
selectorId: "other-element",
closeable: false,
},
];TourProvider supports callbacks for tour lifecycle events. All callbacks receive the tourId as the first argument:
<TourProvider
tours={tours}
onStart={(tourId) => console.log(`${tourId} started`)}
onStepChange={(tourId, step) => console.log(`${tourId} moved to step ${step}`)}
onComplete={(tourId) => console.log(`${tourId} completed`)}
onSkip={(tourId, step) => console.log(`${tourId} skipped at step ${step}`)}
>
{children}
</TourProvider>If a target element is off-screen when its step becomes active, the tour automatically scrolls it into view with a smooth animation before highlighting it.
Configuration for an individual tour step.
| Prop | Type | Default | Description |
|---|---|---|---|
content |
React.ReactNode |
Required | The content to display in the tooltip |
selectorId |
string |
Required | The id of the target element to highlight |
position |
"top" | "bottom" | "left" | "right" |
"bottom" |
Tooltip placement relative to the target |
width |
number |
undefined |
Override the spotlight width |
height |
number |
undefined |
Override the spotlight height |
padding |
number |
8 |
Spotlight padding around the target element |
borderRadius |
number |
8 |
Spotlight corner radius |
showSkip |
boolean |
true |
Whether to show the "Skip tour" button (hidden on last step regardless) |
closeable |
boolean |
undefined |
Override the provider-level closeable setting for this step |
onClickWithinArea |
() => void |
undefined |
Callback when the user clicks within the highlighted area |
Defines a named tour for use with the tours prop.
| Prop | Type | Description |
|---|---|---|
id |
string |
Unique identifier for the tour |
steps |
TourStep[] |
Array of steps in this tour |
A context provider component that manages the tour state and functionality.
| Prop | Type | Default | Description |
|---|---|---|---|
children |
React.ReactNode |
Required | The child components to be wrapped by the provider |
tours |
TourDefinition[] |
undefined |
Array of named tour definitions |
onComplete |
(tourId: string) => void |
undefined |
Callback when a tour is completed |
onStart |
(tourId: string) => void |
undefined |
Callback when a tour starts |
onStepChange |
(tourId: string, step: number) => void |
undefined |
Callback when the active step changes |
onSkip |
(tourId: string, step: number) => void |
undefined |
Callback when a tour is skipped |
closeable |
boolean |
false |
Show a close (X) button on each tooltip |
className |
string |
undefined |
Additional CSS classes for the tour highlight border |
isTourCompleted |
boolean |
false |
Initial state of tour completion |
A hook that provides access to the tour context and functionality. Must be used within a TourProvider component.
| Value | Type | Description |
|---|---|---|
currentStep |
number |
Current step index (-1 when tour is inactive) |
totalSteps |
number |
Total number of steps in the tour |
nextStep |
() => void |
Function to advance to the next step |
previousStep |
() => void |
Function to go back to the previous step |
endTour |
() => void |
Function to end the tour |
isActive |
boolean |
Whether the tour is currently active |
startTour |
(tourId?: string) => void |
Function to start a tour (by ID if using multiple tours) |
setSteps |
(steps: TourStep[]) => void |
Function to set tour steps (for single-tour usage) |
steps |
TourStep[] |
Array of tour step configurations |
isTourCompleted |
boolean |
Whether the tour has been completed |
setIsTourCompleted |
(completed: boolean) => void |
Function to set tour completion status |
activeTourId |
string | null |
The ID of the currently active tour |
const { startTour, currentStep, nextStep, activeTourId } = useTour();A dialog component that prompts users to start or skip the tour.
| Prop | Type | Description |
|---|---|---|
isOpen |
boolean |
Controls the visibility of the dialog |
setIsOpen |
(isOpen: boolean) => void |
Callback to update the dialog's open state |