Interactive image hotspots with zoom, popovers, and accessibility. Zero dependencies.
Live Demo | Visual Editor | Vanilla Sandbox | React Sandbox
Existing hotspot libraries are often heavy, inaccessible, or locked behind paid services. This library was built to fill the gap:
- Lightweight — under 15 KB gzipped with zero runtime dependencies
- Accessible by default — WCAG 2.1 AA compliant out of the box
- Framework-agnostic — works with vanilla JS, React, or any framework
- Built-in zoom & pan — no need for a separate zoom library
- Multi-image scenes — create virtual tours without extra tooling
- Optional Cloudimage CDN — serve optimally-sized images automatically
- Hotspot markers — Positioned via percentage or pixel coordinates with pulsing animation
- Popover system — Hover, click, or load triggers with built-in flip/shift positioning
- Zoom & Pan — CSS transform-based with mouse wheel, pinch-to-zoom, double-click, drag-to-pan
- WCAG 2.1 AA — Full keyboard navigation, ARIA attributes, focus management, reduced motion
- CSS variable theming — Light and dark themes, fully customizable
- Two init methods — JavaScript API and HTML data-attributes
- React wrapper — Separate entry point with component, hook, and ref API
- TypeScript — Full type definitions
- Cloudimage CDN — Optional responsive image loading
- Multi-image scenes — Navigate between images with animated transitions
npm install js-cloudimage-hotspot<script src="https://scaleflex.cloudimg.io/v7/plugins/js-cloudimage-hotspot/1.1.6/js-cloudimage-hotspot.min.js?vh=e5c3c4&func=proxy"></script>import CIHotspot from 'js-cloudimage-hotspot';
const viewer = new CIHotspot('#product-image', {
src: 'https://example.com/living-room.jpg',
alt: 'Modern living room',
zoom: true,
trigger: 'hover',
hotspots: [
{
id: 'sofa',
x: '40%',
y: '60%',
label: 'Modern Sofa',
data: { title: 'Modern Sofa', originalPrice: '$1,099', price: '$899', description: 'Comfortable 3-seat sofa' },
},
{
id: 'lamp',
x: '75%',
y: '25%',
label: 'Floor Lamp',
markerStyle: 'dot-label',
data: { title: 'Arc Floor Lamp', price: '$249' },
},
],
onOpen(hotspot) {
console.log('Opened:', hotspot.id);
},
});<div
data-ci-hotspot-src="https://example.com/room.jpg"
data-ci-hotspot-alt="Living room"
data-ci-hotspot-zoom="true"
data-ci-hotspot-trigger="hover"
data-ci-hotspot-items='[
{"id":"sofa","x":"40%","y":"60%","label":"Sofa","data":{"title":"Sofa","price":"$899"}}
]'
></div>
<script>CIHotspot.autoInit();</script>new CIHotspot(element: HTMLElement | string, config: CIHotspotConfig)| Option | Type | Default | Description |
|---|---|---|---|
src |
string |
— | Image source URL (required) |
hotspots |
HotspotItem[] |
— | Array of hotspot definitions (required) |
alt |
string |
'' |
Image alt text |
trigger |
'hover' | 'click' | 'load' |
'hover' |
Popover trigger mode |
zoom |
boolean |
false |
Enable zoom & pan |
zoomMax |
number |
4 |
Maximum zoom level |
zoomMin |
number |
1 |
Minimum zoom level |
theme |
'light' | 'dark' |
'light' |
Theme |
pulse |
boolean |
true |
Marker pulse animation |
placement |
'top' | 'bottom' | 'left' | 'right' | 'auto' |
'top' |
Popover placement |
lazyLoad |
boolean |
true |
Lazy load image |
zoomControls |
boolean |
true |
Show zoom control buttons |
zoomControlsPosition |
'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right' |
'bottom-right' |
Zoom controls position |
renderPopover |
(hotspot) => string | HTMLElement |
— | Custom popover render |
onOpen |
(hotspot) => void |
— | Popover open callback |
onClose |
(hotspot) => void |
— | Popover close callback |
onZoom |
(level) => void |
— | Zoom change callback |
onClick |
(event, hotspot) => void |
— | Marker click callback |
cloudimage |
CloudimageConfig |
— | Cloudimage CDN config |
scenes |
Scene[] |
— | Array of scenes for multi-image navigation |
initialScene |
string |
first scene | Scene ID to display initially |
sceneTransition |
'fade' | 'slide' | 'none' |
'fade' |
Scene transition animation |
sceneAspectRatio |
string |
— | Fixed viewport ratio (e.g. '16/9'). Prevents layout jumps. |
onSceneChange |
(id, scene) => void |
— | Scene change callback |
| Field | Type | Description |
|---|---|---|
id |
string |
Unique identifier (required) |
x |
string | number |
X coordinate: '65%' or 650 (px) |
y |
string | number |
Y coordinate: '40%' or 400 (px) |
label |
string |
Accessible label (required) |
markerStyle |
'dot' | 'dot-label' |
Marker style — 'dot-label' shows a text pill next to the dot |
data |
PopoverData |
Data for built-in template |
content |
string |
HTML content (sanitized) |
trigger |
'hover' | 'click' | 'load' |
Override global trigger |
placement |
Placement |
Override global placement |
className |
string |
Custom CSS class |
hidden |
boolean |
Initially hidden |
icon |
string |
Custom icon |
navigateTo |
string |
Scene ID to navigate to on click |
| Field | Type | Description |
|---|---|---|
title |
string |
Popover heading |
originalPrice |
string |
Strikethrough price shown before current price (e.g. '$1,499') |
price |
string |
Current price |
description |
string |
Description text |
image |
string |
Image URL displayed at top of popover |
url |
string |
Link URL for the CTA button |
ctaText |
string |
CTA button label (default: 'View details') |
instance.open(id: string): void
instance.close(id: string): void
instance.closeAll(): void
instance.setZoom(level: number): void
instance.getZoom(): number
instance.resetZoom(): void
instance.addHotspot(hotspot: HotspotItem): void
instance.removeHotspot(id: string): void
instance.updateHotspot(id: string, updates: Partial<HotspotItem>): void
instance.update(config: Partial<CIHotspotConfig>): void
instance.destroy(): void
instance.goToScene(sceneId: string): void
instance.getCurrentScene(): string | undefined
instance.getScenes(): string[]CIHotspot.autoInit(root?: HTMLElement): CIHotspotInstance[]import { CIHotspotViewer, useCIHotspot } from 'js-cloudimage-hotspot/react';
// Component
function ProductImage() {
return (
<CIHotspotViewer
src="/living-room.jpg"
alt="Living room"
zoom
hotspots={[
{ id: 'sofa', x: '40%', y: '60%', label: 'Sofa', data: { title: 'Sofa', price: '$899' } },
]}
onOpen={(h) => console.log('Opened:', h.id)}
/>
);
}
// Hook
function ProductImage() {
const { containerRef, instance } = useCIHotspot({
src: '/room.jpg',
hotspots: [...],
zoom: true,
});
return (
<>
<div ref={containerRef} />
<button onClick={() => instance.current?.setZoom(2)}>Zoom 2x</button>
</>
);
}
// Ref API
function ProductImage() {
const ref = useRef<CIHotspotViewerRef>(null);
return (
<>
<CIHotspotViewer ref={ref} src="/room.jpg" hotspots={[...]} zoom />
<button onClick={() => ref.current?.open('sofa')}>Show Sofa</button>
</>
);
}Navigate between multiple images, each with its own hotspots:
const viewer = new CIHotspot('#tour', {
scenes: [
{
id: 'living-room',
src: '/living-room.jpg',
alt: 'Living room',
hotspots: [
{ id: 'sofa', x: '40%', y: '60%', label: 'Sofa', data: { title: 'Modern Sofa' } },
{ id: 'go-kitchen', x: '85%', y: '50%', label: 'Go to Kitchen', navigateTo: 'kitchen' },
],
},
{
id: 'kitchen',
src: '/kitchen.jpg',
alt: 'Kitchen',
hotspots: [
{ id: 'island', x: '50%', y: '65%', label: 'Island', data: { title: 'Marble Island' } },
{ id: 'go-back', x: '10%', y: '50%', label: 'Back', navigateTo: 'living-room' },
],
},
],
sceneTransition: 'fade', // 'fade' | 'slide' | 'none'
});
// Programmatic navigation
viewer.goToScene('kitchen');
viewer.getCurrentScene(); // 'kitchen'
viewer.getScenes(); // ['living-room', 'kitchen']Hotspots with navigateTo display as arrow markers and switch scenes on click.
All visuals are customizable via CSS variables:
.my-viewer {
--ci-hotspot-marker-bg: rgba(0, 88, 163, 0.8);
--ci-hotspot-pulse-color: rgba(0, 88, 163, 0.3);
--ci-hotspot-cta-bg: #e63946;
--ci-hotspot-popover-border-radius: 4px;
--ci-hotspot-popover-text-align: center; /* left (default) | center | right */
}Set theme: 'dark' for the built-in dark theme.
- All markers are
<button>elements witharia-label Tab/Shift+Tabnavigates between markersEnter/Spacetoggles popoversEscapecloses popovers and returns focusArrow keyspan when zoomed+/-/0zoom controls- Focus trapping in popovers with interactive content
prefers-reduced-motiondisables animations
new CIHotspot('#el', {
src: 'https://example.com/room.jpg',
cloudimage: {
token: 'demo',
limitFactor: 100,
params: 'q=80',
},
hotspots: [...],
});| Browser | Version |
|---|---|
| Chrome | 80+ |
| Firefox | 80+ |
| Safari | 14+ |
| Edge | 80+ |
If this library helped your project, consider buying me a coffee!
Made with care by the Scaleflex team
