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.
- π― 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
npm install pixelguide<script src="https://unpkg.com/pixelguide/dist/pixelguide.js"></script><!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>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);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)'
}Start a flow without registering it first.
pg.startFlow({
id: 'quick-tour',
steps: [...]
});Register a flow for later use.
pg.addFlow({
id: 'feature-tour',
steps: [...]
});Start a previously registered flow by ID.
pg.start('feature-tour');Go to the next step.
Go to the previous step.
Cancel/skip the current flow.
Check if a flow is currently active.
Get the currently active flow.
Get the current step index.
Update the theme dynamically.
pg.setTheme({
primaryColor: '#ec4899',
backgroundColor: '#1f2937',
textColor: '#ffffff'
});Clean up and remove all event listeners.
Enable the trigger system after construction.
pg.enableTriggers({
storage: new LocalStorageAdapter(),
debug: true
});Initialize auto-triggers for all registered flows. Call this after adding flows with trigger configurations.
await pg.initializeTriggers();Reset completion status and metadata for a flow. Useful for testing or allowing users to replay tours.
await pg.resetFlowMetadata('welcome-tour');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;
}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)
}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'
}
]
});const pg = new PixelGuide({
theme: {
primaryColor: '#10b981',
backgroundColor: '#1f2937',
textColor: '#f9fafb',
overlayColor: 'rgba(0, 0, 0, 0.8)'
}
});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);
}
});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!'
}
]
});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');
}
}
]
});PixelGuide supports JSON-based configuration, making it easy to load tours from files, CMSs, or backends.
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();// Load from remote JSON file
const pg = await ConfigLoader.fromURL('/configs/tour.json');
await pg.initializeTriggers();const jsonString = `{
"flows": [
{
"id": "welcome",
"steps": [
{ "title": "Welcome", "content": "Hello!", "modal": true }
]
}
]
}`;
const pg = ConfigLoader.fromJSONString(jsonString);const errors = ConfigLoader.validate(config);
if (errors.length > 0) {
console.error('Invalid configuration:', errors);
} else {
const pg = ConfigLoader.fromJSON(config);
}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');
};
}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.
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 });Choose how flow completion and metadata is persisted:
Persists across browser sessions. Best for remembering completed tours long-term.
import { LocalStorageAdapter } from 'pixelguide';
const pg = new PixelGuide({
triggers: {
storage: new LocalStorageAdapter()
}
});Clears when browser tab closes. Good for session-based tours.
import { SessionStorageAdapter } from 'pixelguide';
const pg = new PixelGuide({
triggers: {
storage: new SessionStorageAdapter()
}
});In-memory only, no persistence. Good for testing or temporary state.
import { MemoryStorageAdapter } from 'pixelguide';
const pg = new PixelGuide({
triggers: {
storage: new MemoryStorageAdapter()
}
});Disables all persistence. Flows always behave as if never shown.
import { NoOpStorageAdapter } from 'pixelguide';
const pg = new PixelGuide({
triggers: {
storage: new NoOpStorageAdapter()
}
});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 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 waitstartOn:'immediate'- Start timer immediately'load'- Wait forwindow.loadevent'idle'- Wait for browser idle (usesrequestIdleCallback)
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 todirection-'down','up', or'both'
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)
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)
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 pathnamequeryParams- Object of query parameters to matchhash- Hash fragment to match
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();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();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 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();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();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();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();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();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();// 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();npm installnpm run buildThis creates:
dist/pixelguide.js- IIFE bundle (minified)dist/pixelguide.esm.js- ES module bundle (minified)dist/index.d.ts- TypeScript definitions
npm run devWatch mode for development.
npm run build
# Open example.html in your browser- 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
MIT
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork and clone the repository
- Install dependencies:
npm install - Make your changes
- Run linting:
npm run lint(auto-fix withnpm run lint:fix) - Run tests:
npm test - Build:
npm run build - Submit a PR
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.
- ESLint - Enforces code style and quality rules
- TypeScript - Full type safety
- Vitest - 218+ tests with comprehensive coverage
- Prettier-ready - Consistent code formatting
Built with β€οΈ using Shadow DOM and Floating UI