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

@diabolic/pointy

v1.2.0

Published

A lightweight, dependency-free JavaScript library for creating animated tooltips with a pointing cursor. Perfect for product tours, onboarding flows, and feature highlights.

Downloads

853

Readme

Pointy

A lightweight, dependency-free JavaScript library for creating animated tooltips with a pointing cursor. Perfect for product tours, onboarding flows, and feature highlights.

npm version License: MIT

Live Demo

Features

  • 🎯 Animated Pointer - Smooth cursor animation with customizable SVG
  • 📝 Multi-step Tours - Create guided product tours with multiple steps
  • 💬 Multi-message Steps - Each step can have multiple messages that auto-cycle
  • 🎬 Autoplay Mode - Automatically advance through steps
  • 🎨 Customizable Styling - CSS variables, custom class names, SVG support, and color theming
  • 🎨 Color Theming - Customize pointer, bubble background, and text colors
  • 📍 Target Tracking - Follows target elements in real-time
  • ⚛️ React Compatible - Supports JSX/React elements as content
  • 🔗 Event System - Comprehensive events with group listeners
  • 🌊 Smooth Animations - 11 built-in easing presets
  • 🪶 Lightweight - Zero dependencies, ~15KB

Installation

npm install @diabolic/pointy

CDN

<script src="https://unpkg.com/@diabolic/pointy/dist/pointy.min.js"></script>

Quick Start

import Pointy from '@diabolic/pointy';

const pointy = new Pointy({
  steps: [
    { target: '#welcome-btn', content: 'Click here to get started!' },
    { target: '#features', content: 'Explore our features' },
    { target: '#settings', content: ['Customize your experience', 'Change themes', 'Set preferences'] }
  ]
});

pointy.show();

Configuration

Basic Options

| Option | Type | Default | Description | |--------|------|---------|-------------| | steps | Array | [] | Array of step objects | | target | string\|HTMLElement | null | Initial target (single-step use) | | content | string\|string[] | '' | Initial content (single-step use) |

Step Object

{
  target: '#element',           // CSS selector or HTMLElement
  content: 'Message',           // String, HTML, array, or React element
  direction: 'up-left',         // Direction preset or null (auto)
  duration: 3000                // Step-specific autoplay duration (ms)
}

Content Types

Pointy supports multiple content formats:

// Plain text
{ target: '#el', content: 'Simple message' }

// HTML string with custom layout
{ target: '#el', content: `
  <div style="display: flex; gap: 10px; align-items: flex-start; max-width: 260px; margin: 4px 0;">
    <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
      <circle cx="12" cy="12" r="10"/>
      <path d="M12 16v-4M12 8h.01"/>
    </svg>
    <span style="line-height: 1.4;">Custom tooltip with icon and flexible multi-line text layout!</span>
  </div>
` }

// Multiple messages (auto-cycles)
{ target: '#el', content: ['First message', 'Second message', 'Third message'] }

// React/JSX element (if using React)
{ target: '#el', content: <MyCustomComponent /> }

Direction Presets

Control the pointer and bubble direction manually:

| Direction | Description | |-----------|-------------| | null | Auto (default) - automatically adjusts based on viewport | | 'up' | Pointer points up, bubble below target | | 'down' | Pointer points down, bubble above target | | 'left' | Bubble on left side of target | | 'right' | Bubble on right side of target | | 'up-left' | Pointer up, bubble on left | | 'up-right' | Pointer up, bubble on right | | 'down-left' | Pointer down, bubble on left | | 'down-right' | Pointer down, bubble on right |

// In steps
{ target: '#el', content: 'Hello', direction: 'down-right' }

// Runtime
pointy.setDirection('up-left');           // Both axes
pointy.setHorizontalDirection('right');   // Only horizontal
pointy.setVerticalDirection('down');      // Only vertical
pointy.setDirection(null);                // Reset to auto

// pointTo with direction
pointy.pointTo('#element', 'Message', 'down-left');

Animation

| Option | Type | Default | Description | |--------|------|---------|-------------| | animationDuration | number | 1000 | Move animation duration (ms) | | introFadeDuration | number | 1000 | Initial fade-in duration (ms) | | bubbleFadeDuration | number | 500 | Bubble fade duration (ms) | | easing | string | 'default' | Easing preset or cubic-bezier | | floatingAnimation | boolean | true | Enable floating animation |

Position

| Option | Type | Default | Description | |--------|------|---------|-------------| | offsetX | number | 20 | Horizontal offset from target | | offsetY | number | 16 | Vertical offset from target | | initialPosition | string | 'center' | Starting position preset | | tracking | boolean | true | Enable real-time target tracking | | zIndex | number | 9999 | CSS z-index for the container |

Initial Position Presets: 'center', 'top-left', 'top-center', 'top-right', 'middle-left', 'middle-right', 'bottom-left', 'bottom-center', 'bottom-right', 'first-step'

Autoplay

| Option | Type | Default | Description | |--------|------|---------|-------------| | autoplay | number\|null | null | Auto-advance interval (ms) | | autoplayEnabled | boolean | false | Start autoplay on show | | autoplayWaitForMessages | boolean | true | Wait for all messages | | messageInterval | number\|null | null | Message auto-cycle interval (ms) |

Styling

| Option | Type | Default | Description | |--------|------|---------|-------------| | pointerColor | string | null | Pointer/cursor color (CSS color) | | bubbleBackgroundColor | string | null | Bubble background color (CSS color) | | bubbleTextColor | string | null | Bubble text color (CSS color) | | bubbleMaxWidth | string | 'min(400px, 90vw)' | Maximum bubble width (CSS value) |

Completion

| Option | Type | Default | Description | |--------|------|---------|-------------| | resetOnComplete | boolean | true | Reset to initial position | | hideOnComplete | boolean | true | Auto-hide after completion | | hideOnCompleteDelay | number\|null | null | Delay before hide (ms) |

Callbacks

| Option | Type | Description | |--------|------|-------------| | onStepChange | function(index, step) | Called when step changes | | onComplete | function() | Called when tour completes |

Methods

Lifecycle

pointy.show();        // Show the pointer
pointy.hide();        // Hide the pointer
pointy.destroy();     // Remove and cleanup
pointy.restart();     // Restart from initial position

Navigation

pointy.next();              // Go to next step
pointy.prev();              // Go to previous step
pointy.goToStep(index);     // Go to specific step
pointy.reset();             // Reset to initial position

Content

pointy.setMessage('Single message');            // Replace all with single message
pointy.setMessages(['Message 1', 'Message 2']); // Set multiple messages

pointy.setCurrentMessage('Updated text');       // Update message at current index only

pointy.nextMessage();
pointy.prevMessage();
pointy.goToMessage(index);

Point to Custom Target

pointy.pointTo('#element');
pointy.pointTo('#element', 'Custom message');
pointy.pointTo('#element', 'Message', 'down');

Autoplay Control

pointy.startAutoplay();
pointy.stopAutoplay();
pointy.pauseAutoplay();
pointy.resumeAutoplay();

Message Cycling

pointy.startMessageCycle();
pointy.stopMessageCycle();
pointy.pauseMessageCycle();
pointy.resumeMessageCycle();

Setters

All setters emit corresponding *Change events:

// Animation
pointy.setEasing('bounce');
pointy.setAnimationDuration(800);
pointy.setIntroFadeDuration(500);
pointy.setBubbleFadeDuration(300);
pointy.setMessageTransitionDuration(400);
pointy.setFloatingAnimation(true);

// Position
pointy.setOffset(30, 20);
pointy.setInitialPosition('top-left');
pointy.setInitialPositionOffset(50);
pointy.setZIndex(10000);
pointy.setStayInViewport(true, { x: 50, y: 80 }); // Auto-flip with custom thresholds

// Direction
pointy.setDirection('up-left');           // Set both directions
pointy.setHorizontalDirection('right');   // Only horizontal (left/right/null)
pointy.setVerticalDirection('down');      // Only vertical (up/down/null)

// Tracking
pointy.setTracking(true);
pointy.setTrackingFps(30);

// Messages
pointy.setMessageInterval(2000);

// Autoplay
pointy.setAutoplayInterval(3000);
pointy.setAutoplayWaitForMessages(true);

// Completion
pointy.setResetOnComplete(true);
pointy.setHideOnComplete(true);
pointy.setHideOnCompleteDelay(500);

// Styling
pointy.setPointerSvg('<svg>...</svg>');
pointy.setPointerColor('#ff6600');           // Pointer color
pointy.setBubbleBackgroundColor('#1a1a2e');  // Bubble background
pointy.setBubbleTextColor('#ffffff');        // Bubble text
pointy.setBubbleMaxWidth('300px');           // Max bubble width

Easing Presets

pointy.setEasing('default');    // Smooth deceleration
pointy.setEasing('bounce');     // Bouncy overshoot
pointy.setEasing('elastic');    // Elastic spring
pointy.setEasing('smooth');     // Symmetric ease
pointy.setEasing('snap');       // Quick snap
pointy.setEasing('expo-out');   // Exponential out
pointy.setEasing('back-out');   // Back out

// CSS built-ins
pointy.setEasing('ease');
pointy.setEasing('linear');

// Custom
pointy.setEasing('cubic-bezier(0.68, -0.55, 0.27, 1.55)');

Events

pointy.on('show', (data) => console.log('Shown!'));
pointy.on('stepChange', (data) => console.log(data.toIndex));
pointy.on('complete', (data) => console.log('Done!'));

// Unsubscribe
pointy.off('stepChange', handler);

Event Groups

Listen to multiple related events at once:

pointy.on('lifecycle', (data) => {
  // Fires for: show, hide, destroy, restart, reset
  console.log(data.type);
});

pointy.on('navigation', (data) => {
  // Fires for: stepChange, next, prev, complete
});

pointy.on('all', (data) => {
  // Fires for ALL events
});

Available Groups: lifecycle, navigation, animation, content, messageCycle, pointing, tracking, autoplay, config

All Events

Lifecycle

| Event | Data | |-------|------| | beforeShow | { target } | | show | { target, isIntro, isFirstStep? } | | beforeHide | { target } | | hide | { target } | | destroy | {} | | beforeRestart | {} | | restart | {} | | beforeReset | { currentStep } | | reset | { stepIndex } |

Navigation

| Event | Data | |-------|------| | beforeStepChange | { fromIndex, toIndex, step, fromTarget } | | stepChange | { fromIndex, toIndex, step, target } | | next | { fromIndex, toIndex } | | prev | { fromIndex, toIndex } | | complete | { totalSteps, source } |

Animation

| Event | Data | |-------|------| | animationStart | { fromTarget, toTarget, type, stepIndex? } | | animationEnd | { fromTarget, toTarget, type, stepIndex? } | | move | { index, step } | | moveComplete | { index, step, target } | | introAnimationStart | { duration, initialPosition } | | introAnimationEnd | { initialPosition } | | flipHorizontal | { from: 'left'\|'right', to: 'left'\|'right' } | | flipVertical | { from: 'up'\|'down', to: 'up'\|'down' } |

Direction

| Event | Data | |-------|------| | directionChange | { from: { horizontal, vertical }, to: { horizontal, vertical } } | | horizontalDirectionChange | { from, to } | | verticalDirectionChange | { from, to } |

Content

| Event | Data | |-------|------| | messagesSet | { messages, total, animated, cyclePaused } | | currentMessageUpdate | { index, message, oldMessage, total, animated } | | messageChange | { fromIndex, toIndex, message, total, isAuto? } |

Message Cycle

| Event | Data | |-------|------| | messageCycleStart | { interval, totalMessages } | | messageCycleStop | { currentIndex } | | messageCyclePause | { currentIndex } | | messageCycleResume | { currentIndex } | | messageCycleComplete | { stepIndex, totalMessages } |

Pointing

| Event | Data | |-------|------| | beforePointTo | { target, content, direction, fromTarget } | | pointTo | { target, content, direction } | | pointToComplete | { target, content } |

Tracking

| Event | Data | |-------|------| | track | { target, timestamp } | | targetChange | { from, to } | | trackingChange | { from, to } | | trackingFpsChange | { from, to } |

Autoplay

| Event | Data | |-------|------| | autoplayStart | {} | | autoplayStop | {} | | autoplayPause | {} | | autoplayResume | {} | | autoplayNext | { fromIndex, duration?, afterMessages? } | | autoplayComplete | { totalSteps } | | autoHide | { delay, source } | | autoplayChange | { from, to } | | autoplayWaitForMessagesChange | { from, to } |

Viewport

| Event | Data | |-------|------| | stayInViewportChange | { from: { enabled, x, y }, to: { enabled, x, y } } |

Config

All setter methods emit *Change events with { from, to } data:

| Event | Description | |-------|-------------| | pointerColorChange | Pointer color changed | | bubbleBackgroundColorChange | Bubble background color changed | | bubbleTextColorChange | Bubble text color changed | | bubbleMaxWidthChange | Bubble max width changed | | easingChange | Easing preset changed | | animationDurationChange | Animation duration changed | | floatingAnimationChange | Floating animation toggled | | ... | (and more for all setters) |

CSS Customization

CSS Variables

.pointy-container {
  /* Animation */
  --pointy-duration: 1000ms;
  --pointy-easing: cubic-bezier(0, 0.55, 0.45, 1);
  --pointy-bubble-fade: 500ms;
  
  /* Colors */
  --pointy-pointer-color: #0a1551;
  --pointy-bubble-bg: #0a1551;
  --pointy-bubble-color: white;
  --pointy-bubble-max-width: min(400px, 90vw);
}

Custom Class Names

Customize class prefix or override specific class names:

// Change prefix (default: 'pointy')
const pointy = new Pointy({
  classPrefix: 'my-tour',  // → .my-tour-container, .my-tour-bubble, etc.
  steps: [...]
});

// Override specific suffixes
const pointy = new Pointy({
  classSuffixes: {
    container: 'wrapper',  // → .pointy-wrapper instead of .pointy-container
    bubble: 'tooltip'      // → .pointy-tooltip instead of .pointy-bubble
  },
  steps: [...]
});

// Full class name override
const pointy = new Pointy({
  classNames: {
    container: 'custom-container',
    pointer: 'custom-pointer',
    bubble: 'custom-bubble',
    bubbleText: 'custom-text',
    hidden: 'is-hidden',
    visible: 'is-visible',
    moving: 'is-moving'
  },
  steps: [...]
});

Default Class Names:

| Key | Default | Description | |-----|---------|-------------| | container | pointy-container | Main wrapper element | | pointer | pointy-pointer | Pointer/cursor element | | bubble | pointy-bubble | Message bubble | | bubbleText | pointy-bubble-text | Text inside bubble | | hidden | pointy-hidden | Hidden state | | visible | pointy-visible | Visible state | | moving | pointy-moving | During animation |

CSS Variable Prefix

const pointy = new Pointy({
  cssVarPrefix: 'tour',  // → --tour-duration, --tour-easing, etc.
  steps: [...]
});

Custom Pointer SVG

const pointy = new Pointy({
  pointerSvg: `<svg width="40" height="40">...</svg>`,
  steps: [...]
});

Examples

Basic Tour

const tour = new Pointy({
  steps: [
    { target: '#logo', content: 'Welcome to our app!' },
    { target: '#dashboard', content: 'This is your dashboard' },
    { target: '#settings', content: 'Customize settings here' }
  ]
});

tour.show();

Custom Theming

const tour = new Pointy({
  steps: [
    { target: '#feature', content: 'Check out this feature!' }
  ],
  pointerColor: '#ff6600',
  bubbleBackgroundColor: '#1a1a2e',
  bubbleTextColor: '#ffffff',
  bubbleMaxWidth: '300px'
});

tour.show();

// Or change colors at runtime
tour.setPointerColor('#00ff88');
tour.setBubbleBackgroundColor('#2d2d44');

Autoplay Tour

const tour = new Pointy({
  steps: [
    { target: '#step1', content: 'Step 1' },
    { target: '#step2', content: 'Step 2', duration: 5000 },
    { target: '#step3', content: 'Step 3' }
  ],
  autoplay: 3000,
  autoplayEnabled: true,
  hideOnComplete: true
});

tour.show();

Multi-Message Steps

const tour = new Pointy({
  steps: [{
    target: '#feature',
    content: [
      'Did you know...',
      'You can click this button',
      'To access advanced features!'
    ]
  }],
  messageInterval: 2500
});

tour.show();

Pause on Hover

const tour = new Pointy({
  steps: [...],
  autoplay: 3000,
  autoplayEnabled: true
});

tour.container.addEventListener('mouseenter', () => tour.pauseAutoplay());
tour.container.addEventListener('mouseleave', () => tour.resumeAutoplay());

tour.show();

TypeScript

Full TypeScript support included:

import Pointy, { PointyOptions, PointyStep } from '@diabolic/pointy';

const options: PointyOptions = {
  steps: [{ target: '#btn', content: 'Click!' }]
};

const pointy = new Pointy(options);

pointy.on('stepChange', (data) => {
  console.log(data.fromIndex, data.toIndex);
});

Browser Support

Chrome 60+, Firefox 55+, Safari 12+, Edge 79+

License

MIT License


Made with ❤️ for better user experiences.