Onboarding Tour HTML CSS JS — Product Walkthrough Tooltip

Onboarding Tour · Modals · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

getBoundingClientRect() positioning: viewport-relative coordinates for fixed-position tooltip and highlight box
Spotlight highlight: box-shadow: 0 0 0 9999px rgba(0,0,0,0.55) on sized element — no canvas, no clip-path
Viewport edge clamping: Math.max/min clamp after position computation — tooltip stays 8px inside viewport edges
scrollIntoView: smooth scroll to center target before positioning — handles off-screen tour targets
STEPS array: selector + title + description + placement per step — fully data-driven, no code changes per step
Keyboard navigation: ArrowRight/Enter next, ArrowLeft back, Escape skip — listener added/removed with tour lifecycle
Resize handler: cancelAnimationFrame debounce → rAF → re-read getBoundingClientRect on current target
Step counter: "N / Total" display updates on each step, Prev disabled at step 0, Next→Done at last step

About this UI Snippet

Onboarding Tour — How to Build a Product Walkthrough Tour with getBoundingClientRect Tooltip Positioning in JavaScript

Screenshot of the Onboarding Tour snippet rendered live

An onboarding tour guides new users through a product's key features on their first visit — highlighting UI elements one by one with positioned tooltips that explain each feature. It's the standard pattern for reducing time-to-value in SaaS products: instead of sending users to a help document, you bring the help directly to the UI elements in context.

Building an onboarding tour from scratch requires solving three genuinely hard problems: accurately positioning a tooltip relative to any arbitrary target element in a scrolled, responsive layout; creating a highlight effect that draws attention to the target without disrupting the layout; and managing step state with keyboard accessibility and resize handling.

This snippet builds a complete five-step product tour in plain JavaScript, without Shepherd.js, Intro.js, or any tour library.

Target Element Positioning with getBoundingClientRect

getBoundingClientRect() returns the position and dimensions of an element relative to the viewport: { top, left, bottom, right, width, height }. This is the correct API for overlay positioning — unlike offsetTop/offsetLeft (which give position relative to the nearest positioned ancestor and require traversing the parent chain), getBoundingClientRect() gives viewport coordinates directly.

The tooltip positioning function: const rect = target.getBoundingClientRect(). For placement: 'right': the tooltip left = rect.right + gap, top = rect.top + rect.height/2 - tooltipHeight/2. For placement below: top = rect.bottom + gap, left = rect.left. The tooltip is position: fixed (not absolute) so it correctly overlays the page regardless of scroll position and parent container transforms.

Viewport Edge Clamping

A positioned tooltip can overflow the viewport if the target element is near a screen edge. After computing the target-relative position, the function clamps: left = Math.max(8, Math.min(left, window.innerWidth - tooltipWidth - 8)) and top = Math.max(8, Math.min(top, window.innerHeight - tooltipHeight - 8)). The 8px margin keeps the tooltip off the absolute viewport edge. This clamping runs after every position computation, including on window.resize.

The Highlight Overlay

Drawing attention to the target element uses two layers. First, a full-screen semi-transparent overlay darkens everything. Second, a "spotlight" cut-out reveals the target at full brightness. This is achieved with a CSS box-shadow technique: the highlight element is sized to match the target element (via getBoundingClientRect) and uses box-shadow: 0 0 0 9999px rgba(0,0,0,0.55) — an enormous outer shadow that spreads to fill the entire viewport around the element, leaving the element itself at normal brightness inside the shadow edge. This creates the spotlight effect without canvas drawing or clip-path masking.

The highlight element is position: fixed and updated on each step transition: highlight.style.top = rect.top - 4 + 'px', highlight.style.left = rect.left - 4 + 'px', highlight.style.width = rect.width + 8 + 'px', highlight.style.height = rect.height + 8 + 'px'. The 4px padding creates breathing room around the target. A border-radius: 6px on the highlight matches the target element's corner rounding.

Step State Machine

The tour has five steps, each defined as: { selector: '#id', title: '...', description: '...', placement: 'right' | 'bottom' | 'left' }. A currentStep integer tracks progress. The functions goNext() and goPrev() increment/decrement currentStep and call showStep(currentStep).

showStep(i) queries the target element (document.querySelector(step.selector)), calls getBoundingClientRect(), positions both the highlight and tooltip, updates the step counter ("3 / 5"), enables/disables Prev/Next buttons at boundaries, and scrolls the target into view with target.scrollIntoView({ behavior: 'smooth', block: 'center' }) to ensure off-screen targets are visible before positioning.

Keyboard Navigation

The tour registers a keydown event listener on document for the duration of the tour: ArrowRight and Enter call goNext(), ArrowLeft calls goPrev(), and Escape calls endTour(). The listener is added when the tour starts and removed when it ends. ARIA attributes — aria-live="polite" on the tooltip content and aria-label on navigation buttons — ensure screen readers announce step changes.

Resize Handling

When the window resizes (e.g., responsive breakpoint change or DevTools opening), target element positions change. A window.resize event listener calls positionTooltip(target, placement) using requestAnimationFrame to debounce: let rafId; window.addEventListener('resize', () => { cancelAnimationFrame(rafId); rafId = requestAnimationFrame(reposition); }). This re-reads getBoundingClientRect() on the current step's target and re-positions both the highlight box and tooltip. Without this, the tooltip and highlight would drift from their targets after a responsive layout change.

Ending and Skipping the Tour

The "Skip" button calls endTour() at any step. endTour() removes the overlay, hides the tooltip and highlight, removes the keyboard listener, and optionally calls localStorage.setItem('tour-complete', '1') to prevent the tour from showing again on page reload. The final step's Next button changes to "Done" and also calls endTour().

Build with AI

Build, Understand, Optimize, and Extend It With AI

You don't need to work out the positioning math and the highlight technique on your own. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to walk through exactly how positionTooltip decides between placement right, below, or above by comparing spaceBelow and spaceAbove against the tooltip's measured height, or why the spotlight uses a single box-shadow spread to 9999px instead of a canvas overlay with a cutout. The same assistant can help you optimize it — ask whether the resize listener's requestAnimationFrame debounce is enough on a page with dozens of potential tour targets, or whether repositioning should be skipped entirely while the tooltip is off-screen. It's equally useful for extending the tour: have it add scrollIntoView so off-screen steps auto-scroll into position, support a left or top placement option, or persist tour completion to localStorage so returning users don't see it again. Treat the code less like a finished artifact and more like a starting point for a conversation.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a "product onboarding tour" in plain HTML, CSS, and JavaScript using only getBoundingClientRect for positioning — no third-party tour library like Shepherd, Intro.js, or Driver.js.

Requirements:
- A STEPS array of plain objects, each with a CSS selector for its target element, a title, a description, and an optional placement (e.g. right, or default below/above).
- A highlight technique that adds a class to the current target element giving it box-shadow: 0 0 0 9999px rgba(0,0,0,0.6) (or similar) plus a colored ring shadow, and a temporarily elevated z-index, so the target appears lit against a fully darkened rest-of-page with no separate overlay element and no clip-path.
- A tooltip element positioned with position: fixed, whose top and left are computed from the target's getBoundingClientRect() each step: if placement is right, center it vertically beside the element; otherwise decide between placing it below or above the target based on which side actually has enough remaining viewport space, defaulting to below when both fit.
- After computing the raw position, clamp both left and top so the tooltip never overflows the viewport edges, leaving a small margin (e.g. 8-12px) on all sides.
- A dot-based progress indicat­or and a step counter label ("Step N of Total") that update every time the step changes, plus Previous/Next buttons where Previous is disabled on the first step and Next relabels to "Finish" on the last step.
- Keyboard support: ArrowRight or Enter advances, ArrowLeft goes back, and Escape ends the tour immediately, all wired through a single keydown listener active only while the tour is running.
- A window resize listener that recomputes and reapplies the tooltip's position for whatever step is currently active, so the tooltip and highlight never drift away from their target after a layout change.
- On completing the final step, remove the highlight and show a short-lived success toast confirming the tour is done.

Step by step

How to Use

  1. 1
    Start the tourClick "Start Tour" to begin. The first target element highlights with a spotlight box-shadow and a positioned tooltip appears beside it.
  2. 2
    Navigate forwardClick "Next" or press the right arrow key to advance to the next step. Each step highlights a different UI element and repositions the tooltip.
  3. 3
    Navigate backwardClick "Back" or press the left arrow key to return to the previous step. The highlight and tooltip move back to the previous target.
  4. 4
    Skip or endClick "Skip" at any step to end the tour immediately. On the last step, the Next button becomes "Done" and ends the tour on click.
  5. 5
    Define your stepsEdit the STEPS array: each entry needs a selector (#id or .class), a title, a description, and an optional placement (right, bottom, left). The tour positions itself relative to whatever element matches the selector.
  6. 6
    Persist tour completionAdd localStorage.setItem("tour-done", "1") in endTour(), then check localStorage.getItem("tour-done") before calling startTour() to skip the tour for returning users.

Real-world uses

Common Use Cases

SaaS Product First-Run Onboarding
Guide new users through the key features of a dashboard, project management tool, or analytics platform on their first login. Each step highlights a real UI element and explains its purpose in context. Pair with localStorage persistence to show the tour only once per user.
Interactive Product Demo & Sales Walkthrough
Build a guided demo mode for sales prospects — highlight the product's value-driving features in sequence with polished tooltips. Drive prospects through the exact flow that leads to the "aha moment" without needing a live sales call or screen share.
Feature Announcement for New Releases
Announce a new feature on your existing UI by triggering a tour that highlights just the new element. Users who already know the product see the new feature in context, not in a generic release email. Show the tour once per user per release using versioned localStorage keys.
Admin Panel & Settings Page Orientation
Orient new admins or support staff in a dense admin panel. Complex tools with many controls benefit most from contextual walkthroughs. The spotlight effect makes even the most cluttered admin UI navigable by focusing attention on one element at a time.
getBoundingClientRect & Overlay Positioning Study
Study the complete implementation of tooltip positioning: getBoundingClientRect() for target coordinates, position:fixed for overlay placement, box-shadow spotlight technique, viewport edge clamping, and resize event debouncing with rAF. These techniques apply to any positioned overlay — dropdowns, popovers, context menus.
Mobile App PWA First-Use Tutorial
Walk mobile users through a PWA's gestures and key screens on first install. The viewport clamping and scrollIntoView handling ensure tour steps work correctly on small screens where elements may be partially off-screen or require scrolling to reach.

Got questions?

Frequently Asked Questions

The highlight element is a div sized to match the target element using getBoundingClientRect(). It has box-shadow: 0 0 0 9999px rgba(0,0,0,0.55) — an enormous outer box-shadow that spreads in all directions to fill the entire viewport with a dark overlay. The element itself is left at normal brightness, creating the spotlight effect. A border-radius matches the target element's corners. This technique requires no canvas, no SVG, and no clip-path — just one CSS property on a positioned element.

position:absolute positions relative to the nearest positioned ancestor, which may not be the viewport. If the tour runs inside a container with position:relative and overflow:hidden, an absolute tooltip can be clipped. position:fixed always positions relative to the viewport, regardless of scroll position, parent transforms, or container overflow. Since getBoundingClientRect() also returns viewport-relative coordinates, the two are consistent: fixed + getBoundingClientRect() gives the correct position every time.

In positionTooltip(), add cases for "left" and "top": case "left": left = rect.left - tooltipWidth - gap; top = rect.top + rect.height/2 - tooltipHeight/2; break; case "top": top = rect.top - tooltipHeight - gap; left = rect.left + rect.width/2 - tooltipWidth/2; break;. Then apply the same viewport edge clamping to both. Add the placement to the step definition: { selector: "#id", placement: "left", ... }.

In endTour(): localStorage.setItem("onboarding-v1-done", "1"). In your initialization: if (!localStorage.getItem("onboarding-v1-done")) { startTour(); }. Version the key ("v1") so that a new tour version (new features added) shows to users who already completed the old tour. For server-persisted completion (across devices), save a flag to the user record via your API in endTour() and check it server-side before rendering the tour trigger.

window.addEventListener("resize", handler) fires continuously during a drag-resize. Without debouncing, the handler would call getBoundingClientRect() and update the DOM on every resize pixel, causing performance issues. The rAF debounce: let rafId; const handler = () => { cancelAnimationFrame(rafId); rafId = requestAnimationFrame(reposition); }. cancelAnimationFrame cancels any pending rAF from a previous resize event, and the new rAF schedules one call after the current resize frame. reposition re-reads getBoundingClientRect on the current step target and updates highlight and tooltip positions.

Yes. Export via the JSX, Vue, Angular, or Tailwind buttons on this page. In React, keep the current step index in useState and compute the spotlight position from getBoundingClientRect inside a useLayoutEffect so the highlight measures the target after each render.