@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
Maintainers
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.
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/pointyCDN
<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 positionNavigation
pointy.next(); // Go to next step
pointy.prev(); // Go to previous step
pointy.goToStep(index); // Go to specific step
pointy.reset(); // Reset to initial positionContent
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 widthEasing 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.
