NextStep Tours Skill
Build product tours, onboarding flows, and — most importantly — interactive tutorials with NextStep v2 (nextstepjs) in Next.js. This skill makes the action-gated lesson pattern the default rather than generic welcome popups.
Quick Reference
| Task | Approach | Reference |
|---|---|---|
| Install & wire provider | 3-step Quick Start | this file |
| Define a tour | Tour / Step schema |
this file |
| Multi-page tour | nextRoute / prevRoute |
references/multi-page-tours.md |
| Scrollable container | NextStepViewport |
references/multi-page-tours.md |
| Custom card UI | cardComponent prop |
references/custom-card.md |
| Gate progress on user action | Custom card + validation registry | references/tutorial-authoring.md |
| Full props & types | NextStep / Step / CardComponentProps |
references/api-reference.md |
| Element not found, SSR, z-index | Troubleshooting table | references/troubleshooting.md |
Core principle: A tour that just shows tooltips teaches nothing. A tutorial that requires the user to perform each action before advancing teaches. Reach for the validation-gated pattern whenever the word "tutorial" or "lesson" appears.
Install
npm i nextstepjs motion
# or: pnpm add nextstepjs motion / yarn add nextstepjs motion / bun add nextstepjs motion
motion is the peer dependency (the library formerly known as Framer Motion, used for step transitions).
Pages Router users only: if you hit ES module errors at runtime, see references/troubleshooting.md for the next.config.js fix.
Minimal Working Example (App Router)
Three files. Drop-in ready.
1. lib/tours.ts
import type { Tour } from 'nextstepjs';
export const tours: Tour[] = [
{
tour: 'welcome',
steps: [
{
icon: '👋',
title: 'Welcome',
content: "Let's take a quick look around.",
selector: '#welcome-banner',
side: 'bottom',
showControls: true,
showSkip: true,
pointerPadding: 10,
pointerRadius: 8,
},
{
icon: '🧭',
title: 'Navigation',
content: 'Your main nav lives here.',
selector: '#main-nav',
side: 'right',
showControls: true,
showSkip: true,
},
{
icon: '🎉',
title: 'All set',
content: "You're ready. Explore freely.",
showControls: true,
},
],
},
];
2. app/layout.tsx
import { NextStepProvider, NextStep } from 'nextstepjs';
import { tours } from '@/lib/tours';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<NextStepProvider>
<NextStep steps={tours}>
{children}
</NextStep>
</NextStepProvider>
</body>
</html>
);
}
3. components/start-tour-button.tsx (client component that triggers the tour)
'use client';
import { useNextStep } from 'nextstepjs';
export function StartTourButton() {
const { startNextStep } = useNextStep();
return <button => startNextStep('welcome')}>Take a tour</button>;
}
Render <StartTourButton /> anywhere inside the provider tree. Add id="welcome-banner" and id="main-nav" to the elements the steps target.
The Step Shape
All props a single step supports — required ones first, optional below. Source of truth for every example in this skill.
type Step = {
// ── Required ──────────────────────────────────────────────
icon: React.ReactNode | string | null; // emoji string, JSX, or null
title: string; // step heading
content: React.ReactNode; // body (string or JSX)
// ── Targeting ─────────────────────────────────────────────
selector?: string; // CSS #id of the element to anchor to. Omit for a centered modal step.
side?: 'top' | 'bottom' | 'left' | 'right'
| 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
| 'left-top' | 'left-bottom' | 'right-top' | 'right-bottom';
// ── Pointer (the keyhole highlight) ───────────────────────
pointerPadding?: number; // px of breathing room around target
pointerRadius?: number; // border-radius of keyhole (px)
// ── Default-card controls (ignored if using a custom card) ─
showControls?: boolean; // show Next / Prev buttons
showSkip?: boolean; // show Skip button
// ── Behavior ──────────────────────────────────────────────
blockKeyboardControl?: boolean; // disable ←/→/Esc for this step (use on form-input steps)
disableInteraction?: boolean; // block clicks/hover on the highlighted element (view-only steps)
// ── Multi-page routing ────────────────────────────────────
nextRoute?: string; // route to push when the user clicks Next
prevRoute?: string; // route to push when the user clicks Prev
// ── Scrollable containers ─────────────────────────────────
viewportID?: string; // id of a <NextStepViewport> ancestor; positions relative to that container
};
Gotcha: showControls and showSkip only affect NextStep's default card. If you use a custom cardComponent, you must read step.showControls / step.showSkip yourself and render buttons accordingly (see references/custom-card.md).
Step Patterns
DOM-anchored (most common)
{
icon: '⚙️',
title: 'Settings live here',
content: 'Click the gear to manage your account.',
selector: '#settings-gear',
side: 'left',
showControls: true,
showSkip: true,
}
Unanchored modal (intro / outro)
// No `selector` → renders as a centered modal at the top of the document body.
{
icon: '🎉',
title: 'Welcome aboard',
content: "This 60-second walkthrough will get you started.",
showControls: true,
}
Multi-page / routed step
// First page — Next pushes /settings, NextStep waits for #settings-header to mount before advancing.
{
icon: '📄',
title: 'Open settings',
content: 'Click Next to head to the settings page.',
selector: '#dashboard-header',
side: 'bottom',
showControls: true,
nextRoute: '/settings',
}
Viewport-confined step (inside a scrollable container)
// Wrap the scrollable element:
<div className="overflow-auto max-h-[400px]">
<NextStepViewport id="data-table">
<table>
<tr id="row-42">…</tr>
</table>
</NextStepViewport>
</div>
{
icon: '🔍',
title: 'Row 42',
content: 'Scroll position is handled automatically inside the viewport.',
selector: '#row-42',
side: 'top',
viewportID: 'data-table',
showControls: true,
}
Action-gated tutorial step (use for lessons)
A step that does not advance until the user performs a real action. Requires a custom card backed by a validation registry. See references/tutorial-authoring.md for the full pattern and assets/validation-card.tsx + assets/validation-registry.ts for drop-in code.
// The step itself looks normal:
{
icon: '✏️',
title: 'Type your username',
content: 'Enter at least 3 characters in the field above.',
selector: '#username-field',
side: 'bottom',
showControls: true,
blockKeyboardControl: true, // prevent accidental Esc while typing
}
// The magic lives in the validation registry — keyed by [tourName][stepIndex].
// See references/tutorial-authoring.md.
The useNextStep Hook
Every method and property the hook returns. Import from nextstepjs; must be called inside a 'use client' component.
'use client';
import { useNextStep } from 'nextstepjs';
const {
startNextStep, // (tourName: string) => void — start a specific tour by id
closeNextStep, // () => void — exit the current tour
currentTour, // string | null — id of the active tour, or null
currentStep, // number — 0-indexed step pointer
setCurrentStep, // (step: number, delay?: number) => void — jump to a step (optional ms delay)
isNextStepVisible, // boolean — is the overlay currently rendered
} = useNextStep();
Start on user click
'use client';
import { useNextStep } from 'nextstepjs';
export function HelpButton() {
const { startNextStep } = useNextStep();
return <button => startNextStep('welcome')} aria-label="Start tour">Help</button>;
}
Auto-start for first-time users only
'use client';
import { useEffect } from 'react';
import { useNextStep } from 'nextstepjs';
export function OnboardingAutostart({ userId }: { userId: string }) {
const { startNextStep } = useNextStep();
useEffect(() => {
const key = `onboarded:${userId}`;
if (localStorage.getItem(key) === 'true') return;
startNextStep('welcome');
localStorage.setItem(key, 'true');
}, [userId, startNextStep]);
return null;
}
Never call startNextStep during render — always inside useEffect or an event handler.
Analytics & Lifecycle Callbacks
NextStep accepts four lifecycle props. Wire them into your analytics pipeline for funnel visibility.
'use client';
import { NextStep, NextStepProvider } from 'nextstepjs';
import { tours } from '@/lib/tours';
import { track } from '@/lib/analytics';
export function NextStepShell({ children }: { children: React.ReactNode }) {
return (
<NextStepProvider>
<NextStep
steps={tours}
=> track('tour_started', { tourName })}
tourName) => track('tour_step_viewed', { tourName, step })}
=> track('tour_completed', { tourName })}
tourName) => track('tour_skipped', { tourName, step })}
>
{children}
</NextStep>
</NextStepProvider>
);
}
Use onComplete to persist a "tutorialDone" flag so the tour never auto-replays. Use onSkip with the step index to learn where users abandon — that's your tutorial's weakest link.
Designing Tutorials vs. Welcome Tours
The difference between a forgettable popup parade and a real lesson:
- Gate progression on action, not clicks. If the user can spam Next without doing the thing, they will. Use a custom card + validation registry (
references/tutorial-authoring.md) so "Next" actually requires the user to complete the step's task. - One sentence per step. The UI is the teacher; the card is just the label. If content needs more than one sentence, split the step.
- Always include
showSkip: true. People who know the product hate tours. Let them out instantly. - Set
blockKeyboardControl: trueon input steps. Prevents accidental Esc-to-dismiss while the user is typing into a field the step is highlighting. - Use
disableInteraction: truefor view-only steps. Stops the user from clicking the highlighted thing when you want them to read about it, not activate it. - Multi-page lessons need placeholder anchors. If step N+1 lives on
/settingsand that page fetches data async, render a minimal placeholder element with the step'sidbefore the fetch resolves — otherwisenextRoutenavigates but the step never appears. Details inreferences/multi-page-tours.md. - Branch with separate tours, not conditional steps. For novice-vs-expert paths, define two
Tourobjects and pick which one tostartNextStep()based on user state. One tour = one linear flow. - Resume with
setCurrentStep(index, delay). On mount, read a persisted step index fromlocalStorageand jump there with a small delay to let the DOM settle. Pair withonStepChangeto save progress. - Write analytics events per step, not per tour.
tour_step_viewed+step_failed_validationtells you which specific step is frustrating users.tour_completedalone doesn't. - Localize by generating steps inside a hook. NextStep has no built-in i18n; wrap your
toursdefinition in a function that acceptst()from your i18n library and regenerates when locale changes.
Deeper coverage of each principle, with the full validation registry pattern and 4 real validator archetypes, is in references/tutorial-authoring.md.
Copy-Paste Starters
Drop-in files in assets/:
assets/provider-layout.tsx— complete App Routerlayout.tsxwrapping children inNextStepProvider+NextStepwith a custom card, callbacks, and all overlay props set to sensible defaults.assets/tours.ts— representative multi-tour file: one welcome tour, one feature tour withnextRoute, one gated-lesson tour paired with the validation registry.assets/custom-card.tsx— Tailwind + dark-mode aware card, exercises everyCardComponentPropsfield.assets/validation-card.tsx— variant that reads fromvalidation-registry.ts, awaits the validator, shows inline error on fail.assets/validation-registry.ts— typed[tourName][stepIndex]map with three real validator archetypes (DOM input, async fetch, viewport size).
Copy the files, wire the imports, and adjust selectors/IDs to match your app.
References
references/api-reference.md— Every prop onNextStep,Step,Tour,CardComponentProps, andNavigationAdapter, with types, defaults, and the full exports list. Read this when you need the authoritative signature for any prop.references/custom-card.md— Full paste-ready Tailwind + dark-mode card, thearrowslot, overlay tuning (shadowRgb,shadowOpacity,overlayZIndex,clickThroughOverlay), pointer styling, and accessibility notes.references/multi-page-tours.md—nextRoute/prevRoutelifecycle, the placeholder-anchor pattern for async pages,NextStepViewportfor scrollable containers, and when a customnavigationAdapteris needed.references/tutorial-authoring.md— The differentiating content. Validation registry pattern with four validator archetypes (input, click-tracked, async API, viewport gate), pacing rules, branching strategy, resume-later withlocalStorage+setCurrentStep, analytics taxonomy, and localization viagetLocalizedSteps().references/troubleshooting.md— Problem → Fix table covering element-not-found timing, Pages Router ESM errors, dark mode scope, tour re-firing, z-index conflicts, Strict Mode double-invoke, and hydration.