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

Sidebar
Main content

React props

Configure the component via props in React or via attributes in HTML.

PropTypeDefaultDescription
isOpenbooleanrequiredWhether the tour overlay is visible; control this from parent state
stepsTourStep[]requiredOrdered list of tour steps; each step pins to a CSS selector on the page
onClose() => voidrequiredCalled when the user closes the tour via the X button, ESC key, or skip link
onComplete() => voidundefinedCalled when the user clicks the finish button on the last step; fire conversion events here
startStepnumber0Zero-based index of the step to begin on when the tour opens
showSkipbooleantrueShow a skip link in the tooltip footer; calls onClose when clicked
showProgressbooleantrueShow "Step N of N" progress text above the step title
nextLabelstring"Next"Label for the forward navigation button on non-final steps
backLabelstring"Back"Label for the back button; not shown on the first step
finishLabelstring"Done"Label for the action button on the last step
skipLabelstring"Skip tour"Label for the skip link when showSkip is true
classNamestringundefinedExtra 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;
}