webdrive
v1.1.3
Published
Production-ready, accessible, framework-agnostic TypeScript UI tour and onboarding walkthrough library
Maintainers
Readme
webdrive
A production-ready, framework-agnostic TypeScript UI tour and onboarding walkthrough library with zero runtime dependencies.
webdrive provides guided, step-by-step product walkthroughs and feature tours for modern web applications. It works directly with browser DOM APIs and does not depend on React, Vue, Angular, Svelte, Tailwind CSS, or any other UI framework.
✨ Features
- 🎯 100% Framework-Agnostic — Works seamlessly in Vanilla JavaScript, TypeScript, React, Next.js, Vue, Nuxt, Angular, Svelte, or any DOM environment.
- 🪶 Zero Dependencies — Zero external runtime dependencies. Extremely lightweight and fast.
- 🛡️ Non-Destructive Highlighting — Employs a full-screen SVG cutout mask to highlight elements without altering parent stacking contexts,
z-index,overflow, ortransformstyles. - 📐 Dedicated Positioning Engine — Automatic viewport edge collision detection, intelligent flip fallbacks, coordinate clamping, and arrow alignment.
- ♿ Accessible by Design — Proper dialog semantics (
role="dialog",aria-modal="true",aria-labelledby,aria-describedby), focus trapping, automatic focus restoration, and keyboard navigation. - ⌨️ Keyboard Support —
ArrowRight(Next),ArrowLeft(Previous), andEscape(Close). - 💾 Persistent State — Remember completed tours across sessions with namespaced localStorage support and custom storage adapters.
- ⏳ Dynamic Element Support — Gracefully handles elements that load asynchronously (
missingElementBehavior: "skip" | "stop" | "wait"). - 🌓 Themeable & Dark Mode Ready — Controlled entirely via CSS custom properties. Drop-in compatible with Tailwind CSS and shadcn/ui.
- ⚡ SSR Safe — Zero access to
window,document, orlocalStorageduring module evaluation. Safe to import in Next.js Server Components and Node.js.
📦 Installation
npm install webdriveor with yarn / pnpm / bun:
pnpm add webdrive
# or
yarn add webdrive
# or
bun add webdrive🤖 AI Agent Skill Installation
webdrive includes an official Agent Skill so AI coding assistants (Google Antigravity, Claude Code, Cursor, GitHub Copilot) can automatically help you scaffold, configure, and troubleshoot tours.
Option A: Using WebDrive CLI (Instant)
npx webdrive install-skillThis installs SKILL.md into your project's .agents/skills/webdrive/ folder.
Option B: Using the Agent Skills Ecosystem
npx skills add Abhi-6284/webdrive🚀 Quick Start
1. Import CSS & JavaScript
import { WebDrive } from "webdrive";
import "webdrive/styles.css";
const tour = new WebDrive({
id: "dashboard-tour",
steps: [
{
element: "#sidebar",
title: "Navigation",
description: "Use the sidebar to navigate across the application.",
position: "right",
},
{
element: "#dashboard-stats",
title: "Dashboard Statistics",
description: "View real-time business metrics and transaction velocity.",
position: "bottom",
},
{
element: "#profile-menu",
title: "User Profile",
description: "Configure your personal preferences and organization settings.",
position: "left",
},
],
showProgress: true,
animate: true,
smoothScroll: true,
remember: true,
});
tour.start();🛠️ Step Configuration (WebDriveStep)
Each step in the steps array can be configured with the following properties:
interface WebDriveStep {
/** Target element selector string (e.g., "#sidebar") or HTMLElement */
element: string | HTMLElement;
/** Title displayed in the popover header */
title?: string;
/** Plain text description rendered safely inside the content area */
description?: string;
/** Optional custom HTML content (used when rich HTML is required) */
content?: string;
/** Preferred placement: "top" | "right" | "bottom" | "left" (default: "bottom") */
position?: "top" | "right" | "bottom" | "left";
/** Alignment along target axis: "start" | "center" | "end" (default: "center") */
align?: "start" | "center" | "end";
/** Extra padding around the highlighted cutout in pixels (default: 8) */
padding?: number;
/** Spacing between target and popover in pixels (default: 12) */
offset?: number;
/** Show/hide next button for this step */
showNextButton?: boolean;
/** Show/hide previous button for this step */
showPreviousButton?: boolean;
/** Show/hide close button for this step */
showCloseButton?: boolean;
/** Custom label for Next button */
nextButtonText?: string;
/** Custom label for Previous button */
previousButtonText?: string;
/** Custom label for Done button (on final step) */
doneButtonText?: string;
/** Custom label for Close button */
closeButtonText?: string;
/** Hook called when entering this step */
onEnter?: () => void | Promise<void>;
/** Hook called when leaving this step */
onLeave?: () => void | Promise<void>;
/** Extensible custom properties */
[key: string]: unknown;
}⚙️ WebDrive Configuration Options (WebDriveOptions)
interface WebDriveOptions {
/** Unique ID for the tour (required for persistent completion tracking) */
id?: string;
/** Array of tour steps */
steps: WebDriveStep[];
/** Automatically start the tour on instantiation (if not already completed) */
autoStart?: boolean;
/** Show progress counter in popover footer (e.g. "2 / 5") (default: true) */
showProgress?: boolean;
/** Allow user to close the tour via close button or backdrop click (default: true) */
allowClose?: boolean;
/** Smooth animated transitions between steps and cutout bounds (default: true) */
animate?: boolean;
/** Smoothly scroll target element into viewport center before highlighting (default: true) */
smoothScroll?: boolean;
/** Display darkened backdrop overlay (default: true) */
overlay?: boolean;
/** Opacity of backdrop overlay (default: 0.6) */
overlayOpacity?: number;
/** Backdrop overlay color (default: "rgba(0, 0, 0, 0.6)") */
overlayColor?: string;
/** Base z-index for tour layers (default: 100000) */
zIndex?: number;
/** Default padding around highlighted targets (default: 8) */
stagePadding?: number;
/** Border radius of the cutout hole in pixels (default: 6) */
stageRadius?: number;
/** Enable keyboard navigation: Arrow keys & Escape (default: true) */
keyboardNavigation?: boolean;
/** Close tour when pressing Escape (default: true) */
closeOnEscape?: boolean;
/** Show navigation buttons in footer (default: true) */
showButtons?: boolean;
/** Global label for Next button (default: "Next") */
nextButtonText?: string;
/** Global label for Previous button (default: "Previous") */
previousButtonText?: string;
/** Global label for Done button (default: "Done") */
doneButtonText?: string;
/** Global label for Close button (default: "Close tour") */
closeButtonText?: string;
/** Remember completion status in storage so tour doesn't repeat (default: false) */
remember?: boolean;
/** Custom storage provider adapter (defaults to window.localStorage) */
storage?: WebDriveStorage;
/** Strategy when a step target element is not found: "skip" | "stop" | "wait" (default: "skip") */
missingElementBehavior?: "skip" | "stop" | "wait";
/** Maximum time in milliseconds to wait for a missing element if behavior is "wait" (default: 3000) */
missingElementWaitTimeout?: number;
/** Custom progress text formatter function (e.g. (cur, total) => `Step ${cur} of ${total}`) */
renderProgress?: (current: number, total: number) => string;
/** Callback invoked when tour starts */
onStart?: () => void;
/** Callback invoked when advancing or reversing steps */
onStepChange?: (step: WebDriveStep, index: number) => void;
/** Callback invoked when final step is finished */
onComplete?: () => void;
/** Callback invoked when tour is closed before completion */
onClose?: () => void;
/** Callback invoked when tour is destroyed */
onDestroy?: () => void;
}🕹️ Public API Methods
const tour = new WebDrive(options);
// Starts the tour from the first step (or optional index)
await tour.start(startIndex?: number);
// Stops the active tour and cleans up UI
await tour.stop();
// Moves to the next step (or completes if on the final step)
await tour.next();
// Moves to the previous step
await tour.previous();
// Jumps directly to a specific step index
await tour.goTo(index: number);
// Recalculates positioning (call on window resize, layout shift, or dynamic content)
tour.refresh();
// Completely cleans up all DOM nodes, listeners, timers, and observers
tour.destroy();
// Returns true if the tour is currently active
tour.isActive(): boolean;
// Returns the current active step configuration object or null
tour.getCurrentStep(): WebDriveStep | null;
// Returns the zero-based index of the current step (-1 if inactive)
tour.getCurrentStepIndex(): number;
// Returns true if this tour has already been marked completed in storage
await tour.hasCompleted(): Promise<boolean>;
// Resets completion state for this tour ID
await tour.reset(): Promise<void>;
// Resets completion state for all WebDrive tours stored locally
await tour.resetAll(): Promise<void>;📡 Event System
In addition to configuration callbacks, WebDrive provides a type-safe pub/sub event system:
tour.on("start", () => {
console.log("Tour started");
});
tour.on("stepChange", ({ step, index }) => {
console.log(`Current step index: ${index}, title: ${step.title}`);
});
tour.on("complete", () => {
console.log("Tour completed");
});
tour.on("close", () => {
console.log("Tour was dismissed");
});
tour.on("destroy", () => {
console.log("Tour was destroyed");
});
// Remove listeners with tour.off
const handler = () => { /* ... */ };
tour.on("stepChange", handler);
tour.off("stepChange", handler);🏛️ DOM Architecture & Selectors
WebDrive creates an isolated UI container with data-webdrive-* attributes and namespaced CSS classes:
<div data-webdrive-root class="webdrive-root">
<!-- SVG Cutout Mask Overlay -->
<svg data-webdrive-overlay class="webdrive-overlay">
<defs>
<mask id="webdrive-mask-xyz">
<rect width="100%" height="100%" fill="#ffffff" />
<rect data-webdrive-cutout class="webdrive-cutout" rx="6" ry="6" fill="#000000" />
</mask>
</defs>
<rect width="100%" height="100%" mask="url(#webdrive-mask-xyz)" />
</svg>
<!-- Interactive Stage Boundary -->
<div data-webdrive-stage class="webdrive-stage"></div>
<!-- Popover Dialog Card -->
<div data-webdrive-popover class="webdrive-popover" role="dialog" aria-modal="true">
<div data-webdrive-header class="webdrive-header">
<h2 data-webdrive-title class="webdrive-title" id="webdrive-title"></h2>
<button data-webdrive-close class="webdrive-close" aria-label="Close tour">×</button>
</div>
<div data-webdrive-content class="webdrive-content" id="webdrive-description"></div>
<div data-webdrive-footer class="webdrive-footer">
<button data-webdrive-prev class="webdrive-button webdrive-prev"></button>
<div data-webdrive-progress class="webdrive-progress"></div>
<button data-webdrive-next class="webdrive-button webdrive-next"></button>
</div>
<div data-webdrive-arrow class="webdrive-arrow"></div>
</div>
</div>🎨 Styling & CSS Theme Variables
Override theme variables in your CSS to tailor WebDrive to your brand:
:root {
--webdrive-background: #ffffff;
--webdrive-foreground: #111827;
--webdrive-border: #e5e7eb;
--webdrive-primary: #18181b;
--webdrive-primary-foreground: #ffffff;
--webdrive-muted: #6b7280;
--webdrive-overlay: rgba(0, 0, 0, 0.6);
--webdrive-overlay-opacity: 0.6;
--webdrive-radius: 0.5rem;
--webdrive-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
--webdrive-z-index: 100000;
--webdrive-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}Dark Mode
WebDrive automatically responds to dark mode when .dark or [data-theme="dark"] is present on <html> or <body>, or via system @media (prefers-color-scheme: dark):
.dark {
--webdrive-background: #18181b;
--webdrive-foreground: #f4f4f5;
--webdrive-border: #27272a;
--webdrive-primary: #fafafa;
--webdrive-primary-foreground: #18181b;
--webdrive-muted: #a1a1aa;
--webdrive-overlay: rgba(0, 0, 0, 0.75);
--webdrive-shadow: 0 10px 30px rgba(0, 0, 0, 0.6);
}Tailwind CSS & shadcn/ui Integration
WebDrive does not bundle or require Tailwind CSS. You can easily style the exposed classes with @apply in your global CSS:
.webdrive-popover {
@apply rounded-xl border border-border bg-card text-card-foreground shadow-2xl p-5;
}
.webdrive-title {
@apply text-base font-semibold text-foreground tracking-tight;
}
.webdrive-content {
@apply text-sm text-muted-foreground leading-relaxed;
}
.webdrive-next {
@apply rounded-md bg-primary px-3.5 py-2 text-sm font-medium text-primary-foreground shadow-sm hover:bg-primary/90;
}
.webdrive-prev {
@apply rounded-md border border-input bg-background px-3.5 py-2 text-sm font-medium text-foreground hover:bg-accent;
}🌐 Framework Integrations
React
import { useEffect, useRef } from "react";
import { WebDrive } from "webdrive";
import "webdrive/styles.css";
export function AppTour() {
const tourRef = useRef<WebDrive | null>(null);
useEffect(() => {
tourRef.current = new WebDrive({
id: "react-app-tour",
remember: true,
steps: [
{ element: "#sidebar", title: "Sidebar", description: "Quick navigation links." },
{ element: "#search-bar", title: "Search", description: "Search across all assets." },
],
});
tourRef.current.start();
return () => {
tourRef.current?.destroy();
};
}, []);
return null;
}Next.js (App Router & Pages Router)
webdrive is completely safe for Next.js Server Components. In the App Router, initialize WebDrive inside a client component with "use client":
"use client";
import { useEffect } from "react";
import { WebDrive } from "webdrive";
import "webdrive/styles.css";
export function OnboardingTour() {
useEffect(() => {
const tour = new WebDrive({
id: "nextjs-onboarding",
steps: [
{ element: "#hero", title: "Welcome", description: "Welcome to our Next.js app!" },
],
});
tour.start();
return () => {
tour.destroy();
};
}, []);
return null;
}Vue 3 / Nuxt
<script setup>
import { onMounted, onUnmounted } from "vue";
import { WebDrive } from "webdrive";
import "webdrive/styles.css";
let tour = null;
onMounted(() => {
tour = new WebDrive({
steps: [
{ element: "#vue-nav", title: "Navigation", description: "Explore the app." },
{ element: "#vue-content", title: "Content", description: "Your main dashboard." },
],
});
tour.start();
});
onUnmounted(() => {
tour?.destroy();
});
</script>Angular
import { Component, OnInit, OnDestroy } from "@angular/core";
import { WebDrive } from "webdrive";
@Component({
selector: "app-tour",
template: "",
styleUrls: ["node_modules/webdrive/dist/webdrive.css"],
})
export class TourComponent implements OnInit, OnDestroy {
private tour?: WebDrive;
ngOnInit(): void {
this.tour = new WebDrive({
steps: [
{ element: "#angular-header", title: "Header", description: "Main application header." },
],
});
this.tour.start();
}
ngOnDestroy(): void {
this.tour?.destroy();
}
}Svelte
<script>
import { onMount, onDestroy } from "svelte";
import { WebDrive } from "webdrive";
import "webdrive/styles.css";
let tour;
onMount(() => {
tour = new WebDrive({
steps: [
{ element: "#svelte-intro", title: "Intro", description: "Welcome to Svelte!" }
],
});
tour.start();
});
onDestroy(() => {
tour?.destroy();
});
</script>📄 License
MIT © webdrive
