Skip to content

A lightweight, powerful JavaScript SDK for creating beautiful user onboarding experiences and product tours.

Notifications You must be signed in to change notification settings

windmotion-io/pixelguidejs-sdk

Repository files navigation

🎯 PixelGuide

CI Lint Test npm version License: MIT

A lightweight, powerful JavaScript SDK for creating beautiful user onboarding experiences and product tours. Built with Shadow DOM for style isolation, Floating UI for smart positioning, and bundled as an IIFE for easy integration.

✨ Features

  • 🎯 Smart Positioning - Powered by Floating UI, tooltips automatically adjust to stay visible
  • 🎨 Shadow DOM Isolation - Complete style encapsulation, no CSS conflicts
  • πŸ’‘ Spotlight Effects - Beautiful overlays that highlight target elements
  • πŸ“± Responsive - Works seamlessly across all screen sizes
  • ⚑ Lightweight - Small bundle size, zero dependencies (bundled)
  • 🎭 IIFE Bundle - Drop-in script tag or modern ES modules
  • πŸ”§ Fully Typed - Complete TypeScript support
  • 🎨 Themeable - Customize colors, fonts, and styles
  • πŸ“ JSON Configuration - Load tours from JSON files, CMSs, or backends with validation
  • 🎬 Advanced Triggers - Auto-start flows based on scroll, visibility, events, URLs, and delays
  • πŸ’Ύ Flexible Storage - Choose from localStorage, sessionStorage, in-memory, or custom storage adapters
  • 🎯 Smart Flow Control - Conditional display, completion tracking, cooldowns, and show limits
  • 🧩 Modal Support - Show centered modal dialogs without requiring target elements

πŸ“¦ Installation

Via npm

npm install pixelguide

Via CDN (IIFE)

<script src="https://unpkg.com/pixelguide/dist/pixelguide.js"></script>

πŸš€ Quick Start

Using IIFE (Browser)

<!DOCTYPE html>
<html>
<body>
  <button id="signup">Sign Up</button>

  <script src="dist/pixelguide.js"></script>
  <script>
    const pg = new PixelGuide.PixelGuide();

    pg.startFlow({
      id: 'welcome-tour',
      steps: [
        {
          element: '#signup',
          title: 'Get Started',
          content: 'Click here to create your account!',
          placement: 'bottom'
        }
      ]
    });
  </script>
</body>
</html>

Using ES Modules

import { PixelGuide } from 'pixelguide';

const pg = new PixelGuide({
  showOverlay: true,
  showProgress: true,
});

const onboardingFlow = {
  id: 'onboarding',
  steps: [
    {
      element: '#welcome-section',
      title: 'Welcome! πŸ‘‹',
      content: 'Let\'s take a quick tour of the app.',
      placement: 'bottom',
    },
    {
      element: '#main-feature',
      title: 'Main Feature',
      content: 'This is where the magic happens.',
      placement: 'right',
    },
  ],
  onComplete: () => {
    console.log('Tour completed!');
  },
};

pg.startFlow(onboardingFlow);

πŸ“– API Reference

Constructor

new PixelGuide(config?: PixelGuideConfig)

Configuration Options:

Option Type Default Description
defaultPlacement Placement 'top' Default tooltip placement
showOverlay boolean true Show spotlight overlay
showProgress boolean true Show progress dots
offset number 12 Distance from target element
zIndex number 10000 Z-index for tooltips
theme Partial<Theme> - Custom theme options
triggers TriggerManagerConfig - Enable auto-trigger system (see Advanced Triggers section)
accessibility AccessibilityConfig - Accessibility configuration options

Theme Options:

{
  primaryColor: '#6366f1',
  backgroundColor: '#ffffff',
  textColor: '#1f2937',
  borderRadius: '12px',
  fontFamily: 'system-ui, sans-serif',
  overlayColor: 'rgba(0, 0, 0, 0.5)'
}

Methods

startFlow(flow: Flow): Promise<void>

Start a flow without registering it first.

pg.startFlow({
  id: 'quick-tour',
  steps: [...]
});

addFlow(flow: Flow): void

Register a flow for later use.

pg.addFlow({
  id: 'feature-tour',
  steps: [...]
});

start(flowId: string): Promise<void>

Start a previously registered flow by ID.

pg.start('feature-tour');

next(): Promise<void>

Go to the next step.

prev(): Promise<void>

Go to the previous step.

cancel(): void

Cancel/skip the current flow.

isActive(): boolean

Check if a flow is currently active.

getCurrentFlow(): Flow | null

Get the currently active flow.

getCurrentStepIndex(): number

Get the current step index.

setTheme(theme: Partial<Theme>): void

Update the theme dynamically.

pg.setTheme({
  primaryColor: '#ec4899',
  backgroundColor: '#1f2937',
  textColor: '#ffffff'
});

destroy(): void

Clean up and remove all event listeners.

enableTriggers(config?: TriggerManagerConfig): void

Enable the trigger system after construction.

pg.enableTriggers({
  storage: new LocalStorageAdapter(),
  debug: true
});

initializeTriggers(): Promise<void>

Initialize auto-triggers for all registered flows. Call this after adding flows with trigger configurations.

await pg.initializeTriggers();

resetFlowMetadata(flowId: string): Promise<void>

Reset completion status and metadata for a flow. Useful for testing or allowing users to replay tours.

await pg.resetFlowMetadata('welcome-tour');

Flow Configuration

interface Flow {
  id: string;
  steps: Step[];
  onStart?: (flow: Flow) => void;
  onComplete?: (flow: Flow) => void;
  onCancel?: (flow: Flow) => void;
  onStepChange?: (step: Step, index: number) => void;
  trigger?: FlowTriggerConfig;  // Auto-trigger configuration
}

Flow Trigger Configuration:

interface FlowTriggerConfig {
  shouldShow?: () => boolean | Promise<boolean>;
  trigger?: TriggerCondition | TriggerCondition[];
  trackCompletion?: boolean;
  maxShows?: number;
  cooldownPeriod?: number;  // milliseconds
  priority?: number;
}

Step Configuration

interface Step {
  id?: string;
  element?: string | HTMLElement;  // Optional if modal is true
  title: string;
  content: string;
  modal?: boolean;  // Show as centered modal instead of tooltip
  placement?: 'top' | 'right' | 'bottom' | 'left' | 'top-start' | 'top-end' | ...;
  showOverlay?: boolean;
  className?: string;
  onShow?: (step: Step) => void;
  onHide?: (step: Step) => void;
  dismissOnClickOutside?: boolean;
  enableArrowKeyNavigation?: boolean;  // Enable arrow keys (default: true)
}

🎯 Examples

Basic Tour

const pg = new PixelGuide();

pg.startFlow({
  id: 'intro',
  steps: [
    {
      element: '#header',
      title: 'Navigation',
      content: 'Use this menu to navigate around.',
      placement: 'bottom'
    },
    {
      element: '#dashboard',
      title: 'Dashboard',
      content: 'View your stats here.',
      placement: 'right'
    }
  ]
});

With Custom Theme

const pg = new PixelGuide({
  theme: {
    primaryColor: '#10b981',
    backgroundColor: '#1f2937',
    textColor: '#f9fafb',
    overlayColor: 'rgba(0, 0, 0, 0.8)'
  }
});

With Callbacks

pg.startFlow({
  id: 'onboarding',
  steps: [...],
  onStart: (flow) => {
    console.log('Starting tour:', flow.id);
    analytics.track('tour_started');
  },
  onComplete: (flow) => {
    console.log('Tour completed!');
    analytics.track('tour_completed');
  },
  onCancel: (flow) => {
    console.log('Tour cancelled');
  },
  onStepChange: (step, index) => {
    console.log(`Step ${index + 1}:`, step.title);
  }
});

Dynamic Element Targeting

const button = document.querySelector('#my-button');

pg.startFlow({
  id: 'dynamic',
  steps: [
    {
      element: button, // Pass element directly
      title: 'Dynamic Target',
      content: 'You can target elements dynamically!'
    }
  ]
});

Step-Level Callbacks

pg.startFlow({
  id: 'advanced',
  steps: [
    {
      element: '#feature',
      title: 'New Feature',
      content: 'Check out this new feature!',
      onShow: (step) => {
        console.log('Showing step:', step.title);
        document.querySelector('#feature').classList.add('highlight');
      },
      onHide: (step) => {
        document.querySelector('#feature').classList.remove('highlight');
      }
    }
  ]
});

πŸ“ JSON Configuration

PixelGuide supports JSON-based configuration, making it easy to load tours from files, CMSs, or backends.

Quick Start

import { ConfigLoader } from 'pixelguide';

// From JSON object
const config = {
  config: {
    theme: { primaryColor: '#6366f1' },
    triggers: { storage: 'local' }
  },
  flows: [
    {
      id: 'welcome-tour',
      steps: [
        {
          element: '#welcome',
          title: 'Welcome!',
          content: 'Let\'s get started.',
          placement: 'bottom'
        }
      ]
    }
  ]
};

const pg = ConfigLoader.fromJSON(config);
await pg.initializeTriggers();

Loading from URL

// Load from remote JSON file
const pg = await ConfigLoader.fromURL('/configs/tour.json');
await pg.initializeTriggers();

Loading from String

const jsonString = `{
  "flows": [
    {
      "id": "welcome",
      "steps": [
        { "title": "Welcome", "content": "Hello!", "modal": true }
      ]
    }
  ]
}`;

const pg = ConfigLoader.fromJSONString(jsonString);

Validation

const errors = ConfigLoader.validate(config);
if (errors.length > 0) {
  console.error('Invalid configuration:', errors);
} else {
  const pg = ConfigLoader.fromJSON(config);
}

JSON Configuration Format

See example files:

Storage Adapter Types:

  • 'local' - LocalStorageAdapter
  • 'session' - SessionStorageAdapter
  • 'memory' - MemoryStorageAdapter
  • 'noop' - NoOpStorageAdapter

Note: Callback functions (onStart, onComplete, shouldShow, etc.) cannot be serialized in JSON. Add them programmatically after loading:

const pg = ConfigLoader.fromJSON(config);

// Add callbacks programmatically
const flow = pg.getCurrentFlow();
if (flow) {
  flow.onComplete = () => {
    console.log('Tour completed!');
    analytics.track('tour_completed');
  };
}

🎬 Advanced Triggers & Auto-Start

PixelGuide includes a powerful trigger system that allows flows to automatically start based on various conditions. You can trigger flows based on time delays, user actions, scroll position, element visibility, URL patterns, and more.

Enabling Triggers

Enable triggers by passing a triggers configuration:

import { PixelGuide, LocalStorageAdapter } from 'pixelguide';

const pg = new PixelGuide({
  triggers: {
    storage: new LocalStorageAdapter(), // Default
    storagePrefix: 'pixelguide',        // Default
    debug: false,                        // Enable debug logging
    singleAutoFlow: true,                // Only one auto-flow at a time
    initialCheckDelay: 100               // Delay before checking triggers (ms)
  }
});

Or enable triggers after construction:

const pg = new PixelGuide();
pg.enableTriggers({ debug: true });

Storage Adapters

Choose how flow completion and metadata is persisted:

LocalStorageAdapter (Default)

Persists across browser sessions. Best for remembering completed tours long-term.

import { LocalStorageAdapter } from 'pixelguide';

const pg = new PixelGuide({
  triggers: {
    storage: new LocalStorageAdapter()
  }
});

SessionStorageAdapter

Clears when browser tab closes. Good for session-based tours.

import { SessionStorageAdapter } from 'pixelguide';

const pg = new PixelGuide({
  triggers: {
    storage: new SessionStorageAdapter()
  }
});

MemoryStorageAdapter

In-memory only, no persistence. Good for testing or temporary state.

import { MemoryStorageAdapter } from 'pixelguide';

const pg = new PixelGuide({
  triggers: {
    storage: new MemoryStorageAdapter()
  }
});

NoOpStorageAdapter

Disables all persistence. Flows always behave as if never shown.

import { NoOpStorageAdapter } from 'pixelguide';

const pg = new PixelGuide({
  triggers: {
    storage: new NoOpStorageAdapter()
  }
});

Custom Storage Adapter

Implement your own storage (e.g., API backend, IndexedDB):

class APIStorageAdapter {
  async get(key) {
    const res = await fetch(`/api/user-progress/${key}`);
    return res.ok ? (await res.json()).value : null;
  }

  async set(key, value) {
    await fetch(`/api/user-progress/${key}`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ value })
    });
  }

  async remove(key) {
    await fetch(`/api/user-progress/${key}`, { method: 'DELETE' });
  }

  async clear() {
    await fetch('/api/user-progress', { method: 'DELETE' });
  }
}

const pg = new PixelGuide({
  triggers: {
    storage: new APIStorageAdapter()
  }
});

Trigger Types

1. Delay Trigger

Trigger a flow after a time delay.

pg.addFlow({
  id: 'welcome-tour',
  steps: [...],
  trigger: {
    trigger: {
      type: 'delay',
      delay: 2000,              // 2 seconds
      startOn: 'load'           // 'immediate', 'load', or 'idle'
    }
  }
});

await pg.initializeTriggers();

Options:

  • delay - Milliseconds to wait
  • startOn:
    • 'immediate' - Start timer immediately
    • 'load' - Wait for window.load event
    • 'idle' - Wait for browser idle (uses requestIdleCallback)

2. Scroll Trigger

Trigger when user scrolls to a certain position or element.

// Trigger at 50% scroll
pg.addFlow({
  id: 'feature-highlight',
  steps: [...],
  trigger: {
    trigger: {
      type: 'scroll',
      scrollPercentage: 50,      // Trigger at 50% scroll
      direction: 'down'           // 'down', 'up', or 'both'
    }
  }
});

// Trigger when scrolling to an element
pg.addFlow({
  id: 'pricing-tour',
  steps: [...],
  trigger: {
    trigger: {
      type: 'scroll',
      scrollToElement: '#pricing-section',
      direction: 'down'
    }
  }
});

await pg.initializeTriggers();

Options:

  • element - Element to watch (default: window)
  • scrollPercentage - Trigger at this scroll % (0-100)
  • scrollToElement - Trigger when this element is scrolled to
  • direction - 'down', 'up', or 'both'

3. Visibility Trigger

Trigger when an element becomes visible using IntersectionObserver.

pg.addFlow({
  id: 'feature-tour',
  steps: [...],
  trigger: {
    trigger: {
      type: 'visibility',
      element: '#special-feature',
      threshold: 80,              // 80% visible
      delay: 1000                 // Wait 1s after visible
    }
  }
});

await pg.initializeTriggers();

Options:

  • element - Element to watch (required)
  • threshold - Percentage visible to trigger (0-100, default: 50)
  • delay - Milliseconds to wait after visible (default: 0)

4. Event Trigger

Trigger on any DOM event.

// Trigger on button click
pg.addFlow({
  id: 'feature-demo',
  steps: [...],
  trigger: {
    trigger: {
      type: 'event',
      element: '#demo-button',
      eventName: 'click',
      once: true                  // Remove listener after first trigger
    }
  }
});

// Trigger on input focus
pg.addFlow({
  id: 'search-help',
  steps: [...],
  trigger: {
    trigger: {
      type: 'event',
      element: '#search-input',
      eventName: 'focus'
    }
  }
});

await pg.initializeTriggers();

Options:

  • element - Element to attach listener to (required)
  • eventName - DOM event name (e.g., 'click', 'mouseenter', 'focus')
  • once - Remove listener after first trigger (default: false)

5. URL Trigger

Trigger on specific URL patterns.

// Trigger on specific path
pg.addFlow({
  id: 'dashboard-tour',
  steps: [...],
  trigger: {
    trigger: {
      type: 'url',
      urlPattern: '/dashboard'
    }
  }
});

// Trigger with wildcard
pg.addFlow({
  id: 'product-tour',
  steps: [...],
  trigger: {
    trigger: {
      type: 'url',
      urlPattern: '/products/*'
    }
  }
});

// Trigger with query params
pg.addFlow({
  id: 'onboarding',
  steps: [...],
  trigger: {
    trigger: {
      type: 'url',
      urlPattern: '/welcome',
      queryParams: { new_user: 'true' },
      hash: '#start'
    }
  }
});

await pg.initializeTriggers();

Options:

  • urlPattern - String or RegExp to match against pathname
  • queryParams - Object of query parameters to match
  • hash - Hash fragment to match

Multiple Triggers

A flow can have multiple triggers (OR condition):

pg.addFlow({
  id: 'help-tour',
  steps: [...],
  trigger: {
    trigger: [
      { type: 'event', element: '#help-button', eventName: 'click' },
      { type: 'delay', delay: 30000, startOn: 'idle' },
      { type: 'scroll', scrollPercentage: 75 }
    ]
  }
});

await pg.initializeTriggers();

Flow Control & Conditions

Conditional Display

Use shouldShow to conditionally display flows:

pg.addFlow({
  id: 'premium-tour',
  steps: [...],
  trigger: {
    shouldShow: () => {
      // Only show to premium users
      return user.isPremium && !user.hasSeenPremiumTour;
    },
    trigger: { type: 'delay', delay: 2000, startOn: 'load' }
  }
});

// Async conditions
pg.addFlow({
  id: 'personalized-tour',
  steps: [...],
  trigger: {
    shouldShow: async () => {
      const prefs = await fetchUserPreferences();
      return prefs.showTours;
    },
    trigger: { type: 'delay', delay: 1000, startOn: 'load' }
  }
});

await pg.initializeTriggers();

Track Completion

Prevent flows from showing again after completion:

pg.addFlow({
  id: 'onboarding',
  steps: [...],
  trigger: {
    trackCompletion: true,         // Won't show again after completion
    trigger: { type: 'delay', delay: 1000, startOn: 'load' }
  }
});

await pg.initializeTriggers();

// Reset completion status (for testing)
await pg.resetFlowMetadata('onboarding');

Limit Show Count

Limit how many times a flow can be shown:

pg.addFlow({
  id: 'feature-announcement',
  steps: [...],
  trigger: {
    maxShows: 3,                   // Only show 3 times total
    trigger: { type: 'delay', delay: 2000, startOn: 'load' }
  }
});

await pg.initializeTriggers();

Cooldown Period

Prevent flows from re-triggering too frequently:

pg.addFlow({
  id: 'tips-tour',
  steps: [...],
  trigger: {
    cooldownPeriod: 24 * 60 * 60 * 1000,  // 24 hours
    trigger: { type: 'scroll', scrollPercentage: 50 }
  }
});

await pg.initializeTriggers();

Priority

Control which flow triggers first when multiple are ready:

pg.addFlow({
  id: 'critical-update',
  steps: [...],
  trigger: {
    priority: 100,                 // High priority
    trigger: { type: 'delay', delay: 1000, startOn: 'load' }
  }
});

pg.addFlow({
  id: 'general-tips',
  steps: [...],
  trigger: {
    priority: 1,                   // Low priority
    trigger: { type: 'delay', delay: 1000, startOn: 'load' }
  }
});

await pg.initializeTriggers();

Complete Examples

Welcome Tour for New Users

import { PixelGuide, LocalStorageAdapter } from 'pixelguide';

const pg = new PixelGuide({
  triggers: {
    storage: new LocalStorageAdapter(),
    debug: true
  }
});

pg.addFlow({
  id: 'welcome-tour',
  steps: [
    {
      element: '#dashboard',
      title: 'Welcome to Dashboard! πŸ‘‹',
      content: 'This is your command center.',
      placement: 'bottom'
    },
    {
      element: '#settings',
      title: 'Settings',
      content: 'Customize your experience here.',
      placement: 'left'
    }
  ],
  trigger: {
    // Only show to new users
    shouldShow: () => {
      return !localStorage.getItem('returning_user');
    },
    // Auto-start after page loads
    trigger: {
      type: 'delay',
      delay: 2000,
      startOn: 'load'
    },
    // Don't show again after completion
    trackCompletion: true
  },
  onComplete: () => {
    localStorage.setItem('returning_user', 'true');
    console.log('Welcome tour completed!');
  }
});

await pg.initializeTriggers();

Feature Discovery on Scroll

pg.addFlow({
  id: 'feature-discovery',
  steps: [
    {
      element: '#advanced-feature',
      title: 'Advanced Features',
      content: 'Did you know about these powerful features?',
      placement: 'top'
    }
  ],
  trigger: {
    // Show when user scrolls to feature section
    trigger: {
      type: 'visibility',
      element: '#advanced-feature',
      threshold: 60
    },
    // Show max 2 times
    maxShows: 2,
    // Wait 7 days between shows
    cooldownPeriod: 7 * 24 * 60 * 60 * 1000
  }
});

await pg.initializeTriggers();

Help on Demand

pg.addFlow({
  id: 'contextual-help',
  steps: [
    {
      element: '#complex-form',
      title: 'Need Help?',
      content: 'Let us guide you through this form.',
      placement: 'right'
    }
  ],
  trigger: {
    // Trigger on help button click OR after 30 seconds idle
    trigger: [
      {
        type: 'event',
        element: '#help-button',
        eventName: 'click'
      },
      {
        type: 'delay',
        delay: 30000,
        startOn: 'idle'
      }
    ],
    // Show only if user hasn't completed the form
    shouldShow: () => {
      return !document.querySelector('#complex-form').dataset.completed;
    }
  }
});

await pg.initializeTriggers();

Multi-Page Journey

// Page 1: Homepage
pg.addFlow({
  id: 'homepage-tour',
  steps: [...],
  trigger: {
    trigger: { type: 'url', urlPattern: '/' },
    trackCompletion: true
  }
});

// Page 2: Dashboard
pg.addFlow({
  id: 'dashboard-tour',
  steps: [...],
  trigger: {
    trigger: { type: 'url', urlPattern: '/dashboard' },
    trackCompletion: true,
    // Only show after homepage tour is done
    shouldShow: async () => {
      const metadata = await getFlowMetadata('homepage-tour');
      return metadata.completed;
    }
  }
});

await pg.initializeTriggers();

πŸ› οΈ Development

Setup

npm install

Build

npm run build

This creates:

  • dist/pixelguide.js - IIFE bundle (minified)
  • dist/pixelguide.esm.js - ES module bundle (minified)
  • dist/index.d.ts - TypeScript definitions

Development Mode

npm run dev

Watch mode for development.

Run Example

npm run build
# Open example.html in your browser

πŸ—οΈ Architecture

  • Shadow DOM: All tooltips are rendered in Shadow DOM for complete style isolation
  • Floating UI: Smart positioning with automatic flip and shift
  • IIFE Bundle: Self-contained bundle with all dependencies included
  • Event-driven: Comprehensive callback system for tracking user progress
  • TypeScript: Fully typed for excellent developer experience

πŸ“„ License

MIT

🀝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Development Workflow

  1. Fork and clone the repository
  2. Install dependencies: npm install
  3. Make your changes
  4. Run linting: npm run lint (auto-fix with npm run lint:fix)
  5. Run tests: npm test
  6. Build: npm run build
  7. Submit a PR

CI/CD

The project uses GitHub Actions for continuous integration:

  • Lint Workflow - Runs ESLint on all pushes and PRs
  • Test Workflow - Runs all tests with coverage reporting
  • CI Workflow - Complete pipeline: lint, test, and build

All workflows run on Node.js 18.x and 20.x to ensure compatibility.

Code Quality

  • ESLint - Enforces code style and quality rules
  • TypeScript - Full type safety
  • Vitest - 218+ tests with comprehensive coverage
  • Prettier-ready - Consistent code formatting

πŸ”— Links


Built with ❀️ using Shadow DOM and Floating UI

About

A lightweight, powerful JavaScript SDK for creating beautiful user onboarding experiences and product tours.

Resources

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published