Product Tours
Product tours are multi-step walkthroughs that guide users through a feature or workflow. Each step highlights a specific element on the page with a tooltip-style callout containing a title, body text, optional media, and navigation controls.
Overview
A tour consists of:
- Steps -- Ordered sequence of targeted callouts, each attached to a DOM element
- Start page -- Optional welcome screen shown before step 1
- Finish page -- Optional completion screen shown after the last step
- Progress indicator -- Visual progress bar or step counter
- Navigation -- Next, Previous, and Skip controls with optional keyboard shortcuts
Creating a Tour
Via the Admin Panel
- Navigate to Content > Tours and click Create Tour.
- Enter a tour name and optional description.
- Click Add Step to open the visual element selector.
- Click on the element you want to highlight. The admin overlay captures the CSS selector.
- Write the step title and body content.
- Repeat for each step.
- Configure start/finish pages, trigger rules, and audience targeting.
- Click Publish.
Via the API
POST /v1/admin/tours
Content-Type: application/json
{
"title": "Dashboard Onboarding",
"description": "Introduces new users to the main dashboard",
"steps": [
{
"step_id": "welcome-nav",
"order": 0,
"title": "Navigation Menu",
"body": "<p>Use the sidebar to navigate between sections.</p>",
"element_target": { "data_attribute": "nav-menu" },
"position": "right",
"optional": false
},
{
"step_id": "search-bar",
"order": 1,
"title": "Quick Search",
"body": "<p>Press <b>/</b> to search across all your data.</p>",
"element_target": { "element_id": "global-search" },
"position": "bottom"
},
{
"step_id": "dashboard-widget",
"order": 2,
"title": "Your Dashboard",
"body": "<p>Drag and drop widgets to customise your view.</p>",
"element_target": { "css_selector": ".dashboard-grid" },
"position": "top",
"optional": true
}
],
"start_page": {
"enabled": true,
"title": "Welcome to Your Dashboard",
"body": "<p>Let us show you around. This tour takes about 2 minutes.</p>",
"cta_text": "Start Tour"
},
"finish_page": {
"enabled": true,
"title": "You're All Set!",
"body": "<p>You now know the basics. Explore on your own or revisit this tour anytime from the help menu.</p>",
"cta_text": "Got It"
},
"trigger": {
"trigger_type": "first_visit"
},
"audience": {
"rules": [
{ "attribute": "plan", "operator": "in", "values": ["pro", "enterprise"] }
]
}
}Step Configuration
Each step in a tour supports the following fields:
| Field | Type | Required | Description |
|---|---|---|---|
step_id | string | Yes | Unique identifier for the step |
order | number | Yes | Zero-based position of this step within the tour |
title | string | Yes | Step heading text |
body | string | No | Step body content (supports safe HTML) |
element_target | object | Yes | Identifies the DOM element this step pins to (see below) |
position | string | No | Tooltip position relative to the target element |
optional | boolean | No | If true, the step is skipped when the target element is missing |
media_url / media_type | string | No | Embedded image or video URL + type |
cta_text / cta_url | string | No | Custom call-to-action button label + link |
Element target
element_target must include at least one targeting method. When more than one is given, priority is data_attribute → element_id → css_selector.
| Field | Type | Description |
|---|---|---|
data_attribute | string | Value of a data-spotlight="…" attribute on the target. Recommended — survives refactors better than CSS classes. |
element_id | string | The target's HTML id attribute (without the #). |
css_selector | string | A CSS selector. selector is accepted as an alias. |
description | string | Optional human-readable description shown in the admin overlay's target picker. |
{ "element_target": { "data_attribute": "checkout-button" } }
{ "element_target": { "element_id": "global-search" } }
{ "element_target": { "css_selector": ".btn-primary" } }
{ "element_target": { "selector": ".btn-primary" } } // alias of css_selectorPosition
The position field controls where the tooltip appears relative to the target element:
| Value | Description |
|---|---|
top | Above the element |
bottom | Below the element (default) |
left | To the left of the element |
right | To the right of the element |
auto | Automatically positioned to best fit the viewport |
Positioning is handled by @floating-ui/dom, which dynamically adjusts placement if the preferred position would cause the tooltip to overflow the viewport.
Media
Embed images or videos in a step:
{
"step_id": "export-step",
"order": 0,
"title": "Export Your Data",
"body": "<p>Click here to export as CSV or PDF.</p>",
"element_target": { "element_id": "export-btn" },
"media_url": "https://cdn.example.com/export-screenshot.png",
"media_type": "image"
}{
"media_url": "https://cdn.example.com/export-tutorial.mp4",
"media_type": "video"
}Custom CTA
Override the default "Next" button with a custom call-to-action:
{
"step_id": "invite-team",
"order": 0,
"title": "Invite Your Team",
"body": "<p>Collaboration starts here. Invite your first team member.</p>",
"element_target": { "element_id": "invite-btn" },
"cta_text": "Open Invite Dialog",
"cta_url": "https://example.com/invite"
}cta_text becomes the button label; cta_url opens that URL when clicked. Leave both off for the default "Next" behaviour. For interactive completion (e.g. wait for the user to actually click the target), use the completion field with a click_target criterion — see the Interactive Tours section below.
Start Page
The start page is a centered modal shown before the first step. It sets expectations and gives the user the choice to begin or dismiss.
{
"start_page": {
"enabled": true,
"title": "Welcome to Your Dashboard",
"body": "<p>We will walk you through the key features. Takes about 2 minutes.</p>",
"cta_text": "Start Tour",
"dismiss_text": "Maybe Later",
"media": {
"type": "image",
"src": "https://cdn.example.com/welcome.png",
"alt": "Dashboard overview illustration"
}
}
}TIP
Start pages are optional. For short tours (3 steps or fewer), consider skipping the start page and diving straight into step 1.
Finish Page
The finish page appears after the user completes the final step. Use it for congratulations, next steps, or a call-to-action.
{
"finish_page": {
"enabled": true,
"title": "Tour Complete!",
"body": "<p>You are ready to start using the dashboard.</p>",
"cta_text": "Done",
"cta_url": null,
"confetti": true
}
}Progress Indicator
Tours display a progress indicator showing the user's position and total steps. The progress indicator is rendered inside the Shadow DOM and is not affected by host page styles.
The indicator style is configured per tour:
| Style | Description |
|---|---|
bar | Horizontal progress bar (default) |
dots | Step dots showing current position |
fraction | Text showing "Step 2 of 5" |
none | No progress indicator |
{
"progress_style": "dots"
}The progress indicator uses --spot-progress-bg and --spot-progress-fill theme variables for styling.
Element Targeting and Resilience
Selector Types
Tours support any valid CSS selector. For maximum reliability, use the following priority order:
| Priority | Selector Type | Example | Confidence |
|---|---|---|---|
| 1 | data-spotlight attribute | [data-spotlight='nav-menu'] | High |
| 2 | ID selector | #global-search | Medium |
| 3 | Class/attribute selector | .dashboard-widget[data-type='chart'] | Low |
The admin panel displays a confidence tier icon next to each selector:
- Green --
data-spotlightattributes. Survives CSS refactors. - Yellow -- ID selectors. Stable but may change with refactors.
- Red -- Complex CSS selectors. Brittle, breaks with layout changes.
Missing Targets
When a step's target element cannot be found:
| Tour Setting | Behaviour |
|---|---|
require_all_targets: true | Defer the entire tour. Retry on next page load. |
require_all_targets: false (default) | Skip the step and advance to the next valid step. |
Per-step optional: true | Skip this step even when require_all_targets is true. |
If no steps have valid targets, the tour ends gracefully with a completion page (if configured) or silently dismisses.
WARNING
Missing target elements are reported to the Spotlight backend as targeting.element_not_found events. Check the Selector Health dashboard in the admin panel to monitor targeting failures.
SPA Element Waiting
The SDK waits for target elements using a combination of MutationObserver and polling:
- Immediate check --
querySelectoron the current DOM. - MutationObserver -- Watches for DOM changes (element added by SPA rendering).
- Polling fallback --
querySelectorevery 200ms for edge cases MutationObserver misses. - Timeout -- Gives up after 5 seconds (configurable) and applies skip logic.
Triggers
Tours support all 17 trigger types. Common triggers for tours:
| Trigger | Description |
|---|---|
page_load | Show when the user visits the page (default) |
first_visit | Show only on the user's first visit to this page |
event | Show when a matching event fires |
manual | Show only when Spotlight.start(tourId) is called from your app code. |
idle | Show after N seconds of inactivity |
delay | Show N milliseconds after page load |
See Trigger Types for the full list.
Versioning
Every time you publish a tour, a new version is created with a snapshot of the entire tour state. This enables:
- Version history -- See what changed between publishes
- Rollback -- Restore a previous version as a new draft
- Re-show control -- Decide whether users who completed a previous version should see the updated tour
Force Re-Show
When force_reshow is enabled on a tour, users who completed a previous version see the entire tour again after the new version is published. When disabled (default), users who completed any version are not shown the tour again.
{
"force_reshow": true
}Keyboard Navigation
When allowKeyboardNav is enabled in the SDK init options:
| Key | Action |
|---|---|
ArrowRight / Enter | Next step |
ArrowLeft | Previous step |
Escape | Dismiss tour |
Programmatic Control
// Start a specific tour
Spotlight.start('dashboard-onboarding');
// Listen for tour events
Spotlight.on('tour:start', (data) => {
console.log(`Tour started: ${data.tourName}`);
});
Spotlight.on('tour:complete', (data) => {
console.log(`Tour completed in ${data.durationMs}ms`);
});
Spotlight.on('step:view', (data) => {
console.log(`Step ${data.stepIndex + 1}/${data.totalSteps}: ${data.stepTitle}`);
});
// Stop all active tours
Spotlight.stop();Analytics
Tour analytics are tracked automatically:
| Event | Description |
|---|---|
tour_started | User began the tour |
tour_completed | User finished all steps |
tour_dismissed | User closed the tour early |
step_viewed | User viewed a specific step |
step_skipped | A step was skipped due to missing target |
tour_deferred | Tour was deferred because required targets were missing |
These events are visible in the admin analytics dashboard and accessible via the Events API.