Back to Widget Patterns
v2.6Interactive Tooling
Interactive Product Tour
Step-by-step tooltip tour pinning to DOM elements via CSS selectors. Spotlight effect, keyboard close, and step navigation. A basic implementation; pair with react-joyride for advanced positioning.
Live preview
Rendered with real component code. Each example demonstrates a documented variant.
My Dashboard
Main content
React props
Configure the component via props in React or via attributes in HTML.
| Prop | Type | Default | Description |
|---|---|---|---|
| isOpen | boolean | required | Whether the tour overlay is visible; control this from parent state |
| steps | TourStep[] | required | Ordered list of tour steps; each step pins to a CSS selector on the page |
| onClose | () => void | required | Called when the user closes the tour via the X button, ESC key, or skip link |
| onComplete | () => void | undefined | Called when the user clicks the finish button on the last step; fire conversion events here |
| startStep | number | 0 | Zero-based index of the step to begin on when the tour opens |
| showSkip | boolean | true | Show a skip link in the tooltip footer; calls onClose when clicked |
| showProgress | boolean | true | Show "Step N of N" progress text above the step title |
| nextLabel | string | "Next" | Label for the forward navigation button on non-final steps |
| backLabel | string | "Back" | Label for the back button; not shown on the first step |
| finishLabel | string | "Done" | Label for the action button on the last step |
| skipLabel | string | "Skip tour" | Label for the skip link when showSkip is true |
| className | string | undefined | Extra class applied to the overlay root element |
HTML usage
The HTML variant uses the same shared styles and class names. Drop into any stack.
<link rel="stylesheet" href="path/to/primary-button-cta/styles.css">
<link rel="stylesheet" href="path/to/interactive-product-tour/styles.css">
<!-- Target elements the tour will pin to -->
<header id="dashboard-header">My Dashboard</header>
<nav id="sidebar-nav" class="sidebar">Sidebar</nav>
<main id="main-content">Main content</main>
<!-- Trigger button -->
<button id="start-tour-btn">Start tour</button>
<!-- Tour overlay and tooltip are injected by the script below -->
<script>
(function () {
var steps = [
{
target: '#dashboard-header',
title: 'Welcome',
content: 'This is your dashboard header.',
position: 'bottom',
},
{
target: '#sidebar-nav',
title: 'Navigation',
content: 'Use the sidebar to switch sections.',
position: 'right',
},
{
target: '#main-content',
title: 'Content',
content: 'Your main content appears here.',
position: 'left',
},
];
var currentStep = 0;
var overlay, tooltip;
function getRect(selector) {
var el = document.querySelector(selector);
return el ? el.getBoundingClientRect() : null;
}
function buildTooltip(step, index) {
var rect = getRect(step.target);
var top = rect ? rect.bottom + 12 : window.innerHeight * 0.35;
var left = rect ? rect.left : window.innerWidth / 2 - 160;
overlay = document.createElement('div');
overlay.className = 'ipt__overlay';
overlay.setAttribute('aria-hidden', 'true');
tooltip = document.createElement('div');
tooltip.className = 'ipt__tooltip';
tooltip.setAttribute('role', 'dialog');
tooltip.setAttribute('aria-modal', 'true');
tooltip.style.top = top + 'px';
tooltip.style.left = left + 'px';
tooltip.innerHTML =
'<p class="ipt__progress">Step ' + (index + 1) + ' of ' + steps.length + '</p>' +
'<h2 class="ipt__tooltip-title">' + step.title + '</h2>' +
'<p class="ipt__tooltip-content">' + step.content + '</p>' +
'<div class="ipt__actions">' +
(index > 0 ? '<button class="pbc pbc--outlined pbc--rounded" id="ipt-back">Back</button>' : '') +
'<button class="pbc pbc--solid pbc--rounded" id="ipt-next">' +
(index === steps.length - 1 ? 'Done' : 'Next') +
'</button>' +
'<button class="ipt__skip" id="ipt-skip">Skip tour</button>' +
'</div>';
document.body.appendChild(overlay);
document.body.appendChild(tooltip);
document.getElementById('ipt-next').addEventListener('click', function () {
cleanup();
if (index < steps.length - 1) {
currentStep = index + 1;
buildTooltip(steps[currentStep], currentStep);
}
});
var backBtn = document.getElementById('ipt-back');
if (backBtn) {
backBtn.addEventListener('click', function () {
cleanup();
currentStep = index - 1;
buildTooltip(steps[currentStep], currentStep);
});
}
document.getElementById('ipt-skip').addEventListener('click', cleanup);
}
function cleanup() {
if (overlay) overlay.remove();
if (tooltip) tooltip.remove();
}
document.getElementById('start-tour-btn').addEventListener('click', function () {
currentStep = 0;
buildTooltip(steps[0], 0);
});
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape') cleanup();
});
}());
</script>Customization
Override these CSS custom properties to integrate the component into your brand.
.ipt__overlay {
--ipt-overlay-bg: rgba(0, 0, 0, 0.5);
--ipt-z-index: 9999;
--ipt-spotlight-padding: 6px;
--ipt-tooltip-bg: var(--brand-surface, #ffffff);
--ipt-tooltip-shadow: 0 8px 32px rgba(0, 0, 0, 0.18), 0 2px 8px rgba(0, 0, 0, 0.1);
--ipt-tooltip-padding: 1.5rem;
--ipt-tooltip-radius: 0.75rem;
--ipt-tooltip-width: 320px;
--ipt-tooltip-offset: 12px;
--ipt-text-color: var(--brand-ink, #102542);
--ipt-muted-color: rgba(16, 37, 66, 0.6);
--ipt-accent: var(--brand-accent, #1e5fcf);
--ipt-progress-color: rgba(16, 37, 66, 0.45);
--ipt-title-size: 1rem;
--ipt-content-size: 0.9375rem;
}