Interactive video hotspots with time-based markers, keyframe motion, chapters, and accessibility. Zero dependencies.
Live Demo | Visual Editor | Vanilla Sandbox | React Sandbox
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
| 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) |
npm install js-cloudimage-video-hotspot<script src="https://scaleflex.cloudimg.io/v7/plugins/js-cloudimage-video-hotspot/1.1.2/js-cloudimage-video-hotspot.min.js?vh=59d28e&func=proxy"></script>| 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.
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);
},
});<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>new CIVideoHotspot(element: HTMLElement | string, config: CIVideoHotspotConfig)| 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 |
| 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 |
| 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 |
| 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 |
| 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) |
// 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(): voidCIVideoHotspot.autoInit(root?: HTMLElement): CIVideoHotspotInstance[]import { CIVideoHotspotViewer, useCIVideoHotspot } from 'js-cloudimage-video-hotspot/react';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)}
/>
);
}function ShoppableVideo() {
const { containerRef, instance } = useCIVideoHotspot({
src: '/video.mp4',
hotspots: [...],
});
return (
<>
<div ref={containerRef} />
<button onClick={() => instance.current?.nextHotspot()}>Next</button>
</>
);
}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>
</>
);
}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');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 },
],
}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: [...],
});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.
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);
},
});- All markers are focusable
<button>elements witharia-label - Click-mode popovers use
role="dialog"with focus trapping - Hover-mode popovers use
role="tooltip" - Progress bar:
role="slider"witharia-valuenowandaria-valuetext - Screen reader announcements via ARIA live region
| 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 | Version |
|---|---|
| Chrome | 80+ |
| Firefox | 80+ |
| Safari | 14+ |
| Edge | 80+ |
Made with care by the Scaleflex team
