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

js-cloudimage-video-hotspot

v1.1.2

Published

Interactive video hotspots with time-based markers, popovers, and accessibility

Readme


Why js-cloudimage-video-hotspot?

Turn any video into a shoppable, interactive experience. Hotspots appear, move, and disappear in sync with the video timeline — perfect for product showcases, virtual tours, and interactive storytelling.

  • Lightweight — under 20 KB gzipped with zero runtime dependencies
  • Time-based hotspots — appear and disappear at precise moments in the video
  • Object-tracking keyframes — hotspots follow moving objects at 60 fps
  • Accessible by default — WCAG 2.1 AA compliant out of the box
  • Framework-agnostic — works with vanilla JS, React, or any framework
  • Multi-player support — HTML5, HLS, YouTube, and Vimeo via adapter pattern
  • Shoppable popover cards — built-in product template with gallery, variants, rating, wishlist, and countdown

Features

| Category | Details | |----------|---------| | Hotspots | Time-based visibility, keyframe motion paths (linear & Catmull-Rom), easing functions, entrance/exit animations (fade, scale) | | Popovers | Click or hover trigger, flip/shift auto-positioning, built-in product card template, custom render function | | Product card | Image gallery carousel, star rating, size/color/material variants, wishlist toggle, countdown timer, add-to-cart with analytics | | Chapters | Named video segments, navigation dropdown, progress bar dividers | | Controls | Play/pause, volume, speed (0.5x-2x), time display, fullscreen, hotspot prev/next, timeline hotspot indicators | | Players | HTML5 video, HLS (via hls.js), YouTube IFrame API, Vimeo Player SDK — auto-detected from URL | | Accessibility | Keyboard navigation, ARIA attributes, focus traps, screen reader live region, prefers-reduced-motion | | Theming | Light and dark themes, 40+ CSS custom properties | | React | <CIVideoHotspotViewer> component, useCIVideoHotspot hook, ref API | | Analytics | Unified onAnalytics callback for all interactions (show, click, open, close, CTA, add-to-cart, variant, wishlist) |

Installation

npm install js-cloudimage-video-hotspot

CDN

<script src="https://scaleflex.cloudimg.io/v7/plugins/js-cloudimage-video-hotspot/1.1.2/js-cloudimage-video-hotspot.min.js?vh=c33dff&func=proxy"></script>

Optional peer dependencies

| Package | When needed | |---------|-------------| | hls.js | HLS streams (.m3u8) on non-Safari browsers | | @vimeo/player | Vimeo video URLs | | React / React DOM | React wrapper (/react export) |

YouTube adapter loads the IFrame API from CDN automatically — no install needed.

Quick Start

JavaScript API

import CIVideoHotspot from 'js-cloudimage-video-hotspot';

const player = new CIVideoHotspot('#shoppable-video', {
  src: 'https://example.com/fashion-show.mp4',
  poster: 'https://example.com/fashion-poster.jpg',
  pauseOnInteract: true,
  hotspots: [
    {
      id: 'bag',
      x: '65%',
      y: '40%',
      startTime: 12,
      endTime: 25,
      label: 'Designer Bag',
      data: {
        title: 'Designer Bag',
        price: '$899',
        image: 'https://example.com/bag.jpg',
        url: '/products/bag',
      },
      keyframes: [
        { time: 12, x: 65, y: 40 },
        { time: 18, x: 55, y: 45 },
        { time: 25, x: 70, y: 35 },
      ],
      easing: 'ease-in-out',
    },
    {
      id: 'shoes',
      x: '30%',
      y: '85%',
      startTime: 30,
      endTime: 45,
      label: 'Leather Shoes',
      data: { title: 'Leather Shoes', price: '$349' },
    },
  ],
  onHotspotClick(event, hotspot) {
    console.log('Clicked:', hotspot.id);
  },
});

HTML Data-Attributes

<div
  data-ci-video-hotspot-src="https://example.com/video.mp4"
  data-ci-video-hotspot-poster="https://example.com/poster.jpg"
  data-ci-video-hotspot-theme="dark"
  data-ci-video-hotspot-items='[
    {"id":"bag","x":"65%","y":"40%","startTime":12,"endTime":25,"label":"Bag","data":{"title":"Bag","price":"$899"}}
  ]'
></div>

<script>CIVideoHotspot.autoInit();</script>

API Reference

Constructor

new CIVideoHotspot(element: HTMLElement | string, config: CIVideoHotspotConfig)

Config

| Option | Type | Default | Description | |--------|------|---------|-------------| | src | string | required | Video source URL | | hotspots | VideoHotspotItem[] | required | Array of hotspot definitions | | sources | {src, type}[] | — | Multiple video sources for format fallback | | poster | string | — | Poster image URL | | alt | string | — | Accessible description of the video | | playerType | 'auto' \| 'html5' \| 'hls' \| 'youtube' \| 'vimeo' | 'auto' | Player engine (auto-detected from URL) | | hls | HLSConfig | — | HLS-specific options (enableWorker, startLevel, capLevelToPlayerSize) | | trigger | 'hover' \| 'click' | 'click' | Popover trigger mode | | placement | 'top' \| 'bottom' \| 'left' \| 'right' \| 'auto' | 'top' | Default popover placement | | pauseOnInteract | boolean | true | Pause video on hotspot interaction | | theme | 'light' \| 'dark' | 'light' | Color theme | | pulse | boolean | true | Marker pulse animation | | hotspotAnimation | 'fade' \| 'scale' \| 'none' | 'fade' | Hotspot entrance/exit animation | | timelineIndicators | 'dot' \| 'range' \| 'none' | 'dot' | Hotspot indicators on progress bar | | clickToPlay | boolean | true | Toggle play/pause on click in video area | | controls | boolean | true | Show custom video controls | | fullscreenButton | boolean | true | Show fullscreen button | | hotspotNavigation | boolean | true | Show prev/next hotspot buttons | | chapterNavigation | boolean | true | Show chapter dropdown (requires chapters) | | autoplay | boolean | false | Auto-play video on load | | loop | boolean | false | Loop video | | muted | boolean | false | Mute video (auto-set to true when autoplay: true) | | chapters | VideoChapter[] | — | Chapter definitions | | renderPopover | (hotspot) => string \| HTMLElement | — | Custom popover render function | | renderMarker | (hotspot) => string \| HTMLElement | — | Custom marker render function | | cloudimage | CloudimageConfig | — | Cloudimage CDN config for poster |

Callbacks

| Callback | Signature | Description | |----------|-----------|-------------| | onReady | () => void | Video ready to play | | onPlay | () => void | Video started playing | | onPause | () => void | Video paused | | onTimeUpdate | (currentTime: number) => void | Time update (~4 Hz) | | onHotspotShow | (hotspot) => void | Hotspot became visible | | onHotspotHide | (hotspot) => void | Hotspot became hidden | | onHotspotClick | (event, hotspot) => void | Hotspot marker clicked | | onOpen | (hotspot) => void | Popover opened | | onClose | (hotspot) => void | Popover closed | | onChapterChange | (chapter) => void | Active chapter changed | | onFullscreenChange | (isFullscreen: boolean) => void | Fullscreen state changed | | onAnalytics | (event: AnalyticsEvent) => void | Unified analytics for all interactions |

VideoHotspotItem

| Field | Type | Default | Description | |-------|------|---------|-------------| | id | string | required | Unique identifier | | x | string \| number | required | X coordinate: '65%' or number 0-100 | | y | string \| number | required | Y coordinate: '40%' or number 0-100 | | startTime | number | required | Time in seconds when hotspot appears | | endTime | number | required | Time in seconds when hotspot disappears | | label | string | required | Accessible label (used for aria-label and screen reader) | | keyframes | Keyframe[] | — | Motion keyframes: [{time, x, y}, ...] | | easing | 'linear' \| 'ease-in' \| 'ease-out' \| 'ease-in-out' | 'linear' | Keyframe easing function | | interpolation | 'linear' \| 'catmull-rom' | 'linear' | Interpolation mode (catmull-rom for smooth curves) | | data | PopoverData | — | Data for built-in product card template | | content | string | — | Raw HTML content for popover (sanitized) | | trigger | 'hover' \| 'click' | inherit | Override global trigger | | placement | Placement | inherit | Override global placement | | markerStyle | 'dot' \| 'dot-label' \| 'numbered' | 'dot' | Marker visual style | | className | string | — | Custom CSS class on the marker | | animation | 'fade' \| 'scale' \| 'none' | inherit | Override global animation | | autoOpen | boolean | false | Auto-open popover when hotspot appears | | pauseOnShow | boolean | false | Pause video when hotspot appears | | pauseOnInteract | boolean | inherit | Override global pauseOnInteract | | keepOpen | boolean | false | Keep popover open until explicitly closed | | chapterId | string | — | Associate with a chapter | | onClick | (event, hotspot) => void | — | Custom click handler |

PopoverData (built-in product card)

| Field | Type | Description | |-------|------|-------------| | title | string | Product heading | | price | string | Current price | | originalPrice | string | Strikethrough price | | description | string | Description text | | image | string | Product image URL | | images | string[] | Multiple images for gallery carousel | | url | string | Link URL for the CTA button | | ctaText | string | CTA button label (default: 'View details') | | badge | string | Badge text (e.g. 'New', '-30%') | | rating | number | Star rating (0-5, supports half stars) | | reviewCount | number | Number of reviews | | variants | ProductVariant[] | Size/color/material selectors | | wishlist | boolean | Show wishlist button | | wishlisted | boolean | Initial wishlisted state | | countdown | string \| Date | Countdown end date (ISO string or Date) | | countdownLabel | string | Label above the countdown timer | | currency | string | Currency symbol ('$', 'EUR') | | secondaryCta | {text, url?, onClick?} | Secondary CTA button | | customFields | {label, value}[] | Custom key-value fields below description | | sku | string | Product SKU for cart events | | onAddToCart | (event: AddToCartEvent) => void | Add-to-cart callback | | onWishlistToggle | (wishlisted, hotspot) => void | Wishlist toggle callback | | onVariantSelect | (variant, allSelected, hotspot) => void | Variant selected callback |

VideoChapter

| Field | Type | Description | |-------|------|-------------| | id | string | Unique chapter identifier | | title | string | Chapter display title | | startTime | number | Start time in seconds | | endTime | number | End time (optional — defaults to next chapter start or video duration) |

Instance Methods

// Video playback
player.play(): Promise<void>
player.pause(): void
player.togglePlay(): void
player.seek(time: number): void
player.getCurrentTime(): number
player.getDuration(): number
player.setVolume(level: number): void    // 0-1
player.getVolume(): number
player.setMuted(muted: boolean): void
player.isMuted(): boolean
player.setPlaybackRate(rate: number): void
player.getPlaybackRate(): number
player.getVideoElement(): HTMLVideoElement | null  // null for YouTube/Vimeo

// Hotspot management
player.open(id: string): void
player.close(id: string): void
player.closeAll(): void
player.addHotspot(hotspot: VideoHotspotItem): void
player.removeHotspot(id: string): void
player.updateHotspot(id: string, updates: Partial<VideoHotspotItem>): void
player.getVisibleHotspots(): string[]             // returns visible hotspot IDs
player.getHotspots(): VideoHotspotItem[]           // returns all hotspot definitions

// Navigation
player.nextHotspot(): void
player.prevHotspot(): void
player.goToHotspot(id: string): void
player.goToChapter(id: string): void
player.getCurrentChapter(): string | undefined     // returns chapter ID

// Fullscreen
player.enterFullscreen(): void
player.exitFullscreen(): void
player.isFullscreen(): boolean

// DOM access
player.getElements(): { container, videoWrapper, video, overlay, controls }

// Lifecycle
player.update(config: Partial<CIVideoHotspotConfig>): void
player.destroy(): void

Static Methods

CIVideoHotspot.autoInit(root?: HTMLElement): CIVideoHotspotInstance[]

React

import { CIVideoHotspotViewer, useCIVideoHotspot } from 'js-cloudimage-video-hotspot/react';

Component

function ShoppableVideo() {
  return (
    <CIVideoHotspotViewer
      src="/fashion-show.mp4"
      poster="/poster.jpg"
      pauseOnInteract
      hotspots={[
        {
          id: 'bag',
          x: '65%',
          y: '40%',
          startTime: 12,
          endTime: 25,
          label: 'Designer Bag',
          data: { title: 'Designer Bag', price: '$899' },
        },
      ]}
      onHotspotClick={(e, h) => console.log('Clicked:', h.id)}
    />
  );
}

Hook

function ShoppableVideo() {
  const { containerRef, instance } = useCIVideoHotspot({
    src: '/video.mp4',
    hotspots: [...],
  });

  return (
    <>
      <div ref={containerRef} />
      <button onClick={() => instance.current?.nextHotspot()}>Next</button>
    </>
  );
}

Ref API

function ShoppableVideo() {
  const ref = useRef<CIVideoHotspotInstance | null>(null);
  return (
    <>
      <CIVideoHotspotViewer ref={ref} src="/video.mp4" hotspots={[...]} />
      <button onClick={() => ref.current?.goToHotspot('bag')}>Show Bag</button>
    </>
  );
}

Chapters

const player = new CIVideoHotspot('#el', {
  src: '/product-tour.mp4',
  chapters: [
    { id: 'intro', title: 'Introduction', startTime: 0 },
    { id: 'features', title: 'Key Features', startTime: 30 },
    { id: 'pricing', title: 'Pricing', startTime: 90 },
  ],
  hotspots: [
    { id: 'h1', x: '50%', y: '50%', startTime: 35, endTime: 50, label: 'Feature A', chapterId: 'features' },
    { id: 'h2', x: '30%', y: '70%', startTime: 95, endTime: 110, label: 'Plan B', chapterId: 'pricing' },
  ],
});

player.goToChapter('features');

Keyframe Motion

Hotspots can follow moving objects by defining motion keyframes. The plugin interpolates between keyframes at 60 fps using requestAnimationFrame.

{
  id: 'bag',
  x: '50%', y: '50%',
  startTime: 10, endTime: 30,
  label: 'Designer Bag',
  easing: 'ease-in-out',
  interpolation: 'catmull-rom', // smooth curves (default: 'linear')
  keyframes: [
    { time: 10, x: 50, y: 50 },
    { time: 15, x: 40, y: 55 },
    { time: 20, x: 35, y: 60 },
    { time: 25, x: 45, y: 50 },
    { time: 30, x: 55, y: 45 },
  ],
}

Multi-Player Support

The player engine is auto-detected from the source URL, or set explicitly via playerType:

// HLS stream — uses hls.js on Chrome/Firefox, native HLS on Safari
new CIVideoHotspot('#el', {
  src: 'https://example.com/stream.m3u8',
  hotspots: [...],
});

// YouTube
new CIVideoHotspot('#el', {
  src: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
  hotspots: [...],
});

// Vimeo
new CIVideoHotspot('#el', {
  src: 'https://vimeo.com/123456789',
  hotspots: [...],
});

Theming

All visuals are customizable via CSS variables:

.my-player {
  --ci-video-hotspot-marker-size: 32px;
  --ci-video-hotspot-marker-bg: rgba(0, 88, 163, 0.8);
  --ci-video-hotspot-pulse-color: rgba(0, 88, 163, 0.3);
  --ci-video-hotspot-popover-bg: #ffffff;
  --ci-video-hotspot-popover-border-radius: 12px;
  --ci-video-hotspot-cta-bg: #e63946;
  --ci-video-hotspot-controls-bg: rgba(0, 0, 0, 0.8);
  --ci-video-hotspot-progress-color: #ff6b35;
  --ci-video-hotspot-indicator-color: #ffd700;
}

Set theme: 'dark' for the built-in dark theme.

Analytics

Track all interactions through a single callback:

new CIVideoHotspot('#el', {
  src: '/video.mp4',
  hotspots: [...],
  onAnalytics(event) {
    // event.type: 'hotspot_show' | 'hotspot_click' | 'popover_open' | 'popover_close'
    //           | 'cta_click' | 'add_to_cart' | 'variant_select' | 'wishlist_toggle'
    // event.hotspotId, event.timestamp, event.videoTime, event.data
    analytics.track(event.type, event);
  },
});

Accessibility

  • All markers are focusable <button> elements with aria-label
  • Click-mode popovers use role="dialog" with focus trapping
  • Hover-mode popovers use role="tooltip"
  • Progress bar: role="slider" with aria-valuenow and aria-valuetext
  • Screen reader announcements via ARIA live region

Keyboard shortcuts

| Key | Action | |-----|--------| | Space / K | Play / pause | | Left / Right | Seek -5s / +5s | | Up / Down | Volume up / down | | N / P | Next / previous hotspot | | F | Toggle fullscreen | | M | Toggle mute | | Escape | Close popovers or exit fullscreen | | Tab / Shift+Tab | Navigate between markers |

Animations are disabled automatically when prefers-reduced-motion: reduce is set.

Browser Support

| Browser | Version | |---------|---------| | Chrome | 80+ | | Firefox | 80+ | | Safari | 14+ | | Edge | 80+ |

License

MIT