npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

webdrive

v1.1.3

Published

Production-ready, accessible, framework-agnostic TypeScript UI tour and onboarding walkthrough library

Readme

webdrive

A production-ready, framework-agnostic TypeScript UI tour and onboarding walkthrough library with zero runtime dependencies.

npm version License: MIT TypeScript

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, or transform styles.
  • 📐 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 SupportArrowRight (Next), ArrowLeft (Previous), and Escape (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, or localStorage during module evaluation. Safe to import in Next.js Server Components and Node.js.

📦 Installation

npm install webdrive

or 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-skill

This 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">&times;</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