ui-029

Product Onboarding Tour

£10 or the whole Vault, £99

A product onboarding tour: the page dims except a spotlight that scrolls to each step and morphs between them, with a card that flips to the side with room.

intermediate
7 more details
spotlightmask-revealmorphingdialog-animationkeyboard-navigationfocus-managementanchor-scroll

This element reads better at your own screen size than in the frame below:

Updated

About this component

A first-run tour for an app: the page dims, one control stays fully lit inside a spotlight cut out of the dim, and a card explains it. Next and Back morph the cutout to the next target, scrolling it into view first when it sits off screen, and the card follows to whichever side has room, with an arrow pointing at the target. On a phone the card docks to the bottom of the screen.

Read the full effect overview

A tour is data: Tour.start(steps, options) with a target, title, body and placement per step, or data-tour-step attributes on the elements themselves. It returns next, back, goTo, end and destroy, and fires tour:step and tour:end events that say whether the reader finished or skipped. The cutout measures its target live every frame, so it stays attached through scrolling, resizing and reflow. Arrow keys, Escape, focus moved into the card and returned, and a page that is inert behind the dim. Light and dark themes on one data-variant attribute, core GSAP only.

Product Onboarding Tour - UI component preview

What's included

10 items
  • Steps as data: Tour.start(steps, options), or data-tour-step attributes with no JavaScript steps
  • Targets outside the viewport scroll into view before the cutout lands, instantly under reduced motion
  • Spotlight is a hole in an SVG mask, so the highlighted control stays at full contrast
  • Cutout morphs toward the target's live position every frame, so it never lags or detaches during scroll and resize
  • Card auto-flips to the side with room, stays inside the viewport, and its arrow points at the target
  • On a phone the card docks full width to the bottom of the screen
  • next, back, goTo, end and destroy, plus tour:step and tour:end events reporting completed or skipped
  • Progress text, arrow keys, Escape to end, focus moved into the card and returned when the tour ends
  • The page behind is inert to the pointer, with an interactive option that lets the spotlit target be used
  • Light and dark themes on one data-variant attribute

Perfect for

4 use cases
  • First-run onboarding in a dashboard or admin console
  • Feature announcements pointing at the control that changed
  • Guided walkthroughs of long settings pages where later steps sit below the fold
  • In-app help that highlights the step a user is stuck on, recording whether they finished

How it works

3 sections

A hole in the dim

The overlay is one SVG: a full-size rect filled with the theme's dim colour, masked by a white rect with a black rounded rect punched through it. Black in a mask means fully transparent, so the target underneath keeps its own colours at full contrast instead of sitting behind a lightened patch. The ring is written with the same numbers, so the outline and the hole never drift apart.

Scroll first, then morph

A step change works out where the target needs to be: inside the viewport, clear of a fixed header via scroll-padding-top, above a docked card on a phone, and centred with the card when there is room. Scrollable panels and then the window scroll on one tween, and the morph starts a beat behind. Each frame it blends toward the target's current rectangle rather than a snapshot, so it lands on the target however the page moved.

Placement, tracking and focus

The card's side is chosen from where the target will be after the scroll, so it never flips mid-travel; if the reader later scrolls the room away it glides to the side that fits, clamped to the viewport, its arrow sliding to stay on the target. Scroll, resize and ResizeObserver only mark the frame dirty, and one ticker callback measures once per frame. Focus moves into the card, Tab cycles there, Escape ends the tour, and focus returns to where it was.

Difficulty Intermediate
Includes HTML + JS + CSS source, documentation, AI setup prompt, lifetime updates

Lighthouse, as measured

Google Lighthouse on this UI element's demo, from the latest scan. A measurement of the demo as shipped, not a promise for your page.

Accessibility
100
Best practices
100

No performance score, on purpose. That figure depends on how you deploy: your server's compression and caching, your CDN, the connection and the device doing the test, none of which the code controls. The same page can score very differently on two consecutive runs, so measure it where it will live.

Paid UI element

Purchase to unlock the code.

Buying Product Onboarding Tour opens the HTML, CSS and JavaScript source, the full documentation, an AI setup prompt for your editor, and every update we ship to it. Standard license: unlimited personal and commercial projects. Every paid product is tested for desktop and mobile layouts, reduced-motion handling, animation cleanup and keyboard controls wherever there is something to operate.

£10 Standard license, unlimited projects

Browse UI elements

Or the whole library: the Vault, £99, one payment.

Documentation

What's Included

  • index.html: the demo page, wired through the public API

  • assets/style.css: component styles, plain CSS custom properties for colours

  • assets/script.js: readable, commented source; defines one global, Tour

  • Steps are data: Tour.start(steps, options) with a target, title, body, placement, padding and radius per step

  • Or no JavaScript steps at all: data-tour-step attributes on the elements you want to point at

  • A target outside the viewport scrolls into view first, smoothly, and instantly under reduced motion

  • Spotlight is a hole in an SVG mask, so the highlighted control stays at full contrast

  • Cutout morphs position and size together, measuring the target live every frame, so it lands on the target even while the page is still scrolling

  • The cutout and card track the target through scrolling, resizing and layout changes, measured once per animation frame

  • The card picks the side with room (or the side you ask for, flipping when it does not fit), stays inside the viewport, and its arrow points at the target

  • On a phone the card docks full width to the bottom of the screen (to the top when the target cannot scroll clear of it)

  • tour:step and tour:end events; tour:end says whether the reader finished or skipped

  • "2 of 5" progress, arrow keys, Escape to end, focus moved into the card and returned when the tour ends

  • The page behind is inert to the pointer; clicking the dim ends the tour

Quick Start

Code snippet omitted: it ships with the download.

There is no markup to copy. The script builds the overlay and the card, appends them to <body> for the life of the tour, and removes them when it ends.

Declarative steps. Mark the elements and call Tour.start() with no steps:

Code snippet omitted: it ships with the download.

Code snippet omitted: it ships with the download.

Steps run in data-tour-step order, then document order. data-tour-padding and data-tour-radius are also read.

Demo furniture. A tour has to have something to point at, so the demo page carries a search field, a button, a switch row, an invoice table and an Export button below the fold. Everything with a .stage- class, and the toolbar strip across the top with its status line, Restart and Light/Dark buttons, is demo scaffolding. None of it ships with the component, and assets/script.js never reads one of those classes.

API

Tour.start(steps?, options?)

Starts a tour and returns a controller, or null if no step has a target on the page. Starting a tour while another is open destroys the first (only one dim can own the page).

Each step:

Field Type Default What it does
target selector, element, or function returning one none What to spotlight. A selector that matches nothing drops the step, so one tour can span optional UI. Leave it out for a centred card with no spotlight (a welcome or a closing step)
title string '' Card heading (set as text, never HTML). Hidden when empty
body string '' Card text
placement 'auto', 'top', 'right', 'bottom', 'left' 'auto' Preferred side. auto tries bottom, top, right, left; a named side falls back to its opposite, then the others
padding number (px) --spotlight-pad Space between the target and the cutout edge
radius number (px) --spotlight-radius Cutout corner radius

Options:

Option Default What it does
startAt 0 Step index to open on
interactive false Let the pointer through the spotlight to the target (the rest of the page stays inert)
closeOnBackdrop true Clicking the dim ends the tour as skipped. false makes Skip, Escape or Done the only ways out
returnFocus true Move focus back to whatever was focused when the tour started
labels { next: 'Next', back: 'Back', done: 'Done', skip: 'Skip', count: fn } Button text; count(current, total) returns the progress text ('2 of 5')
root document Where declarative data-tour-step elements are collected from
onStep(detail) none Same detail as the tour:step event
onEnd(detail) none Same detail as the tour:end event

Controller

Member What it does
next() Next step; on the last step, ends the tour as completed
back() Previous step
goTo(i) Jump to step i (clamped)
end() End the tour as skipped, with the fade out
destroy() End immediately: no fade, overlay and card removed, every listener and tween reverted. Fires tour:end with reason: 'destroy' if the tour was still open. Safe to call twice
index, total, isOpen Read-only state
steps The steps actually in the tour, after missing targets were dropped

Events

Dispatched on document:

  • tour:step: detail = { index, total, step, target, tour }, after each step change including the first.
  • tour:end: detail = { completed, reason, index, total, tour }. completed is true only when the reader pressed Done. reason is 'done', 'skip' (Skip or end()), 'escape', 'backdrop' or 'destroy'.

Code snippet omitted: it ships with the download.

Tour.active() returns the open controller or null; Tour.collect(root) returns the steps the declarative path would use.

Upgrading from the first version

The first version found #tourOverlay, #tourHole, #tourRing, #tourCard and its buttons by id, and kept its steps in an array inside assets/script.js, exposing window.startTour() and window.endTour(). Delete that markup from your page (the script now generates it with the same .tour-* classes, so your CSS overrides still apply), move your steps array into a Tour.start(steps) call, and replace startTour() with Tour.start(steps) and endTour() with tour.end().

How It Works

A hole in the dim. The overlay is one SVG: a full-size rect filled with --dim, masked by a white rect with a black rounded rect punched through it. Black in a mask means fully transparent, so the target underneath keeps its own colours at full contrast instead of sitting behind a lightened patch. The ring is a second rect written with the same numbers, so outline and hole can never drift apart.

Scroll first, then morph. On a step change the script works out where the target will need to be: inside the viewport, below any fixed header (it reads scroll-padding-top from <html>), above a docked card on a phone, and, when there is room, centred together with the card. Scrollable panels between the target and the page are scrolled first, then the window, on one GSAP tween (instant under reduced motion). The morph starts a beat behind the scroll. A wheel or touch on the dim hands the scroll back to the reader.

Live measurement, not a snapshot. The morph does not tween to numbers taken when it started. Each frame it blends from where the cutout was toward the target's current rectangle, so a target still scrolling into place, a window being resized, or content reflowing above the target all end with the cutout on the target. Scroll, resize, ResizeObserver and the tweens only mark the frame dirty; one gsap.ticker callback measures and writes, once per frame, and does nothing when nothing changed.

Placement. The side is chosen from where the target will be after the scroll, not where it is before, so the card does not flip halfway through. Once settled, if scrolling or resizing takes the room away, the card glides to the side that fits. Its position is clamped to the viewport, and the arrow slides along the card edge to stay on the centre of the spotlight, kept clear of the corners. Below 640px the card is full width at the bottom of the screen with no arrow.

Inert page. A transparent layer under the dim takes every pointer event, so nothing behind the tour can be clicked, including the spotlit control. With interactive: true a matching hole is clipped out of that layer so the target can be used. The wheel still scrolls the page, and the spotlight follows.

Keyboard and ARIA

  • The card is role="dialog" aria-modal="true", labelled by its title and described by its body. The title and body sit in a polite live region, so each step's copy is announced as it changes.
  • Right arrow advances, Left arrow goes back (ignored while typing in a field), Escape ends the tour as skipped.
  • Tab and Shift+Tab cycle inside the card; focus that leaves the card any other way is brought back (into the target is allowed when interactive: true).
  • Focus moves to Next when the tour opens and returns to the previously focused element when it ends. If Back is focused when it becomes disabled, focus moves to Next.
  • The overlay and the pointer layer are aria-hidden="true".
  • Back is disabled on the first step, and Next becomes Done on the last.

Themes

Two themes, one design: light (the default) and dark. Set the attribute on <body> in the demo, or on the component's own root in your page.

Code snippet omitted: it ships with the download.

Every colour, radius and shadow the tour uses is a custom property defined in both body[data-variant="light"] and body[data-variant="dark"] in assets/style.css. Nothing in assets/script.js reads the theme name, so re-valuing one block is the whole of a rebrand:

Token What it is
--ground, --ground-2, --raised, --hover surfaces, back to front
--ink, --ink-2, --ink-3 primary, secondary and muted text
--line, --line-strong hairlines
--accent, --accent-ink the ring, Next, the focus ring, and text on the accent
--dim the colour of the dimmed page outside the spotlight
--ring, --ring-w the outline drawn around the cutout
--card-bg, --card-line, --card-shadow the card and its arrow
--radius, --radius-sm 10px on the card, 6px on controls
--spotlight-pad, --spotlight-radius unitless numbers read by the script at the moment it measures, so a theme can tighten the cutout or square its corners without touching JavaScript; a step's own padding and radius win

The demo's Light/Dark toggle and the ?variant=dark URL parameter just set that attribute; neither is part of the component. The overlay reads the tokens live, so switching theme mid-tour recolours it in place.

Customisation

  • Colours, radii and fonts: the two body[data-variant] blocks in assets/style.css; the accent is #635bff.
  • Travel: the morph runs 0.55s (longer when a long scroll leads it) on power3.inOut; the scroll runs 0.4s to 0.9s by distance on power2.inOut.
  • Card offsets: GAP is the space between spotlight and card, EDGE the minimum margin from the viewport, ARROW_INSET how close the arrow may get to a card corner, all at the top of assets/script.js.
  • Phone docking: DOCK_QUERY at the top of the script and the matching @media (max-width: 640px) block in the stylesheet. Change both together.
  • Fixed headers: set scroll-padding-top on <html> to the header's height and targets scroll clear of it (the demo does this for its 44px toolbar).
  • First run only: call Tour.start after a check of your own, and record the outcome from tour:end.
  • Stacking: the pointer layer and overlay sit at z-index 900 and 901, the card at 950. Anything above 950 (the demo toolbar is 1000) stays clickable during a tour.
  • Reduced motion: the scroll, morph and fades all land instantly.

Requirements

  • GSAP 3.12+ (demo uses 3.15.0), core only
  • No build step, no framework

Worked examples, the events and programmatic API, and the class reference ship with the download, alongside the full source.