wts-tour
v1.0.0
Published
Framework-agnostic, dependency-free product tours and user onboarding for JavaScript and TypeScript, with accessible UI, Web Components, themes, async targets, and persistence.
Keywords
Readme
wts-tour — Framework-Agnostic Product Tours
Build accessible product tours, guided walkthroughs, feature introductions, and
user-onboarding flows with a dependency-free JavaScript and TypeScript library.
wts-tour targets regular DOM elements, so the same API works with plain HTML,
Web Components, Angular, React, Vue, Svelte, or any other frontend framework.
Why wts-tour?
- Framework agnostic: one DOM-based controller for every frontend stack.
- Dependency free: no framework runtime, positioning library, or CSS framework required.
- Accessible by default: modal isolation, focus trapping, keyboard navigation, ARIA announcements, and reduced-motion support.
- Modern onboarding UI: spotlight overlay, target arrow, progress, themes, responsive navigation, and configurable transitions.
- Built for dynamic applications: lazy selectors, async target waiting, hidden-target recovery, and versioned progress persistence.
- Developer friendly: TypeScript declarations, ESM and CommonJS builds,
standalone CSS, Web Component
::part()hooks, and lifecycle events.
Install
npm install wts-tourController API
import { WtsTour } from 'wts-tour';
const tour = new WtsTour(
[
{
target: '#search',
badge: 'Getting started',
title: 'Search',
description: 'Find anything from here.',
},
{
target: document.querySelector('#profile')!,
content: 'Manage your profile and preferences.',
placement: 'bottom',
},
],
{
positionPreference: 'right',
theme: 'auto',
showArrow: true,
transition: 'slide',
escapeToClose: true,
persistence: {
key: 'main-onboarding',
version: 1,
resume: true,
},
onFinish: ({ total }) => {
console.log(`Completed ${total} steps`);
},
},
);
tour.on('change', ({ index }) => {
console.log('Current step', index);
});
await tour.startAsync();
// Clean up when your page/component unmounts.
tour.destroy();Targets may be CSS selectors, DOM Elements, or lazy functions returning an Element. Lazy targets are useful when a framework renders a target later. Missing, detached, and visually hidden targets are skipped during navigation. Counters, bullets, and the Finish button reflect only the currently available steps.
Prefer badge, title, and description for structured content. Their values
are rendered as text and are safe for user-controlled strings. The optional
content field accepts trusted HTML, a DOM Node, or a lazy content function.
Targets rendered later
The synchronous methods remain useful when every target is already mounted. Their async equivalents can wait for targets created by route changes, lazy components, or animations:
const tour = new WtsTour(
[
{
target: '#lazy-panel',
title: 'Your report is ready',
waitForTarget: 5_000,
},
],
{ waitForTarget: 2_000 },
);
await tour.startAsync();
await tour.nextAsync();
await tour.previousAsync();
await tour.goToAsync(2);waitForTarget is measured in milliseconds. A step-level value overrides the
tour default. Waiting is cancelled when the tour is closed, destroyed, or
reconfigured.
Resume and completion
Persistence is opt-in and stores only the current index, completion state, version, and update time:
const tour = new WtsTour(steps, {
persistence: {
key: 'account-onboarding',
version: 2,
storage: 'local', // "local", "session", or a Storage-compatible object
resume: true,
},
});
await tour.startAsync(); // resumes automatically when resume is true
tour.completed; // true after Finish
await tour.resumeAsync();
tour.resetProgress();Changing version ignores progress written by an older tour definition.
Web Component
Importing the element entry registers <wts-tour>.
<script type="module">
import 'wts-tour/element';
</script>
<button id="search">Search</button>
<button id="profile">Profile</button>
<wts-tour
id="onboarding"
position="bottom"
theme="auto"
transition="slide"
wait-for-target="2000"
persistence-key="main-onboarding"
persistence-version="1"
resume
show-counter
show-arrow
>
<wts-tour-step
target="#search"
badge="Getting started"
title="Search"
description="Find anything from here."
></wts-tour-step>
<wts-tour-step target="#profile">
Manage your account here.
</wts-tour-step>
</wts-tour>
<script type="module">
await document.querySelector('#onboarding').startAsync();
</script>You can also assign steps from JavaScript:
const element = document.querySelector('wts-tour');
element.steps = [
{ target: '#search', content: 'Search the site.' },
];
element.start();The element emits composed DOM events named wts-tour-start,
wts-tour-change, wts-tour-next, wts-tour-previous, wts-tour-skip, and
wts-tour-finish.
Framework examples
Angular:
import {
afterNextRender,
Component,
DestroyRef,
inject,
} from '@angular/core';
import { WtsTour } from 'wts-tour';
@Component({ selector: 'app-page', templateUrl: './page.html' })
export class Page {
private readonly tour = new WtsTour([
{ target: '#search', content: 'Search the site.' },
]);
constructor() {
afterNextRender(() => this.tour.start());
inject(DestroyRef).onDestroy(() => this.tour.destroy());
}
}React:
import { useEffect } from 'react';
import { WtsTour } from 'wts-tour';
export function Page() {
useEffect(() => {
const tour = new WtsTour([
{ target: '#search', content: 'Search the site.' },
]);
tour.start();
return () => tour.destroy();
}, []);
return <button id="search">Search</button>;
}Options
Common options include:
positionPreference:top,bottom,left, orrightgap,viewportPadding, andhighlightPaddingkeyboardNavigationandescapeToClosemodal,showOverlay,showBullet, andshowSlideCountershowProgressandshowCloseButtonshowArrowandarrowSizetheme:light,dark,auto, orminimaltransition:scale,slide,fade, ornonetransitionDurationandtransitionEasingwaitForTargetand per-step target wait overridespersistencefor versioned resume and completion statehidePrevious,hideSkip, andhideDonenavigationPositionandskipButtonPositionlabels, or the legacy individual label fieldsscrollBehavior,scrollLock, andcustomClass- lifecycle callbacks such as
onStart,onChange, andonFinish onErrorfor errors thrown by lazy targets, content, labels, callbacks, or event listeners
Tours are modal by default: focus and pointer interaction stay in the tour
while background content is inert. Set modal: false for a non-blocking tour.
Arrow-key navigation does not intercept typing or selection inside form fields
and editable content.
Options can be changed while a tour is active:
tour.updateOptions({
portal: document.querySelector('#tour-layer')!,
injectStyles: false,
modal: false,
});Portal changes within the same document, style injection, scroll locking, and modal isolation are applied immediately. Target and popup size changes are observed automatically.
You can force a measurement after an application-owned layout change with
tour.refresh().
Motion
Step transitions use the browser's Web Animations API and automatically turn off when the user prefers reduced motion:
const tour = new WtsTour(steps, {
transition: 'slide',
transitionDuration: 240,
transitionEasing: 'cubic-bezier(0.22, 1, 0.36, 1)',
theme: 'auto',
showArrow: true,
arrowSize: 10,
showProgress: true,
showCloseButton: true,
});The popup footer wraps into stable metadata and action groups. On narrow cards, the groups stack without horizontal scrolling; long step content scrolls independently so navigation remains visible.
See the exported WtsTourOptions and TourStep types for the complete API.
Styling and CSP
Default styles are injected into the tour root. Customize them with CSS
variables on .wts-tour:
.wts-tour {
--wts-tour-accent: #7c3aed;
--wts-tour-popup-background: #111827;
--wts-tour-popup-color: #f9fafb;
--wts-tour-popup-border: rgba(255, 255, 255, .12);
--wts-tour-overlay: rgba(0, 0, 0, .78);
--wts-tour-highlight-filter: blur(20px);
--wts-tour-popup-width: 360px;
--wts-tour-arrow-background: #111827;
--wts-tour-button-background: transparent;
--wts-tour-primary-background: #7c3aed;
--wts-tour-bullet-size: 8px;
--wts-tour-progress-height: 3px;
--wts-tour-duration: 240ms;
--wts-tour-easing: cubic-bezier(0.22, 1, 0.36, 1);
}--wts-tour-highlight-filter accepts any CSS filter value and defaults to
blur(20px) for a soft spotlight. Set it to none for a crisp cutout, reduce
the blur for a tighter glow, or combine filters such as
blur(12px) brightness(1.08). Larger blur values can require more GPU work on
low-powered devices.
The custom element exposes its Shadow DOM through ::part():
wts-tour::part(popup) {
border-radius: 24px;
}
wts-tour::part(primary-button) {
font-weight: 700;
}
wts-tour::part(arrow) {
filter: drop-shadow(0 4px 5px rgb(0 0 0 / .18));
}Available parts are portal, root, highlight, arrow, popup, content,
badge, title, description, navigation, navigation-meta,
navigation-actions, counter, button, skip-button, previous-button,
primary-button, next-button, finish-button, bullets, bullet,
progress, progress-bar, and close-button.
For a strict Content Security Policy, disable inline style injection and import the stylesheet:
import 'wts-tour/styles.css';
const tour = new WtsTour(steps, { injectStyles: false });Browser and SSR behavior
The package is safe to import during server-side rendering. Create and start a tour only after the browser DOM has mounted. The distributed JavaScript targets modern evergreen browsers and ships ESM, CommonJS, and TypeScript declarations.
Development
npm test
npm run check
npx playwright install
npm run test:browserThe browser suite runs the built package in Chromium, Firefox, and WebKit and checks responsive overflow, arrow rendering, delayed targets, and persisted resume/completion.
See MIGRATION.md when upgrading from the Angular-only 0.x package.
