Interactive image hotspots for React. Define spots in code, render anything, navigate between scenes with animations.
npm install react-image-spotsRequires React 17+. No runtime dependencies.
Switch to mode="edit" during development. Click anywhere on the image — the x/y position is instantly copied to your clipboard.
<ImageSpotMap
mode='edit'
src='/my-photo.jpg'
onSpotPlace={(pos, index) => console.log(pos)}
// pos = { x: 32.5, y: 41.2 }
/>Positions are in % so they scale responsively. On hover, other spots fade out automatically.
import { ImageSpotMap } from 'react-image-spots'
import type { SpotDef } from 'react-image-spots'
const spots: SpotDef[] = [
{
id: 'kitchen',
position: { x: 32.5, y: 41.2 },
size: { w: 5, h: 5 },
render: ({ isHovered, onMouseEnter, onMouseLeave, onClick }) => (
<div
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onClick={onClick}
style={{
width: '100%',
height: '100%',
borderRadius: '50%',
background: isHovered ? '#3b82f6' : 'white',
border: '2px solid #3b82f6',
}}
/>
),
},
]
<ImageSpotMap src="/floor-plan.jpg" spots={spots} />Add hoverSrc or activeSrc to swap the main image on hover or click.
{
id: 'room',
position: { x: 40, y: 50 },
hoverSrc: '/room-hover.jpg',
activeSrc: '/room-click.jpg',
render: (props) => <MySpot {...props} />,
}10 built-in presets or custom CSS keyframes. Set globally or per-spot — per-spot overrides global.
// Global
<ImageSpotMap src="/map.jpg" spots={spots} swapAnimation="zoom" />
// Per-spot override
{
id: 'bridge',
hoverSrc: '/bridge.jpg',
swapAnimation: 'glitch',
render: (props) => <MySpot {...props} />,
}
// Custom keyframes
<ImageSpotMap
src="/map.jpg"
spots={spots}
swapAnimation={{
enter: "my-anim 400ms ease forwards",
leave: "my-exit 400ms ease forwards",
duration: 400,
}}
/>Available presets: fade blur zoom zoom-out slide-up slide-down slide-left slide-right flip glitch none
Navigate between multiple images with smooth transitions.
import { SceneChain } from 'react-image-spots'
import type { SceneDef } from 'react-image-spots'
const scenes: Record<string, SceneDef> = {
world: {
src: '/world-map.jpg',
spots: [
{
id: 'castle',
position: { x: 45, y: 35 },
size: { w: 6, h: 6 },
render: ({ onMouseEnter, onMouseLeave, onClick, goTo }) => (
<button
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onClick={(e) => { onClick(e); goTo('castle') }}
style={{ width: '100%', height: '100%', borderRadius: '50%' }}
/>
),
},
],
},
castle: { src: '/castle.jpg', spots: [] },
}
<SceneChain
scenes={scenes}
initialScene="world"
transition="fade"
showBreadcrumb={false}
renderBackButton={({ goBack }) => (
<button onClick={goBack} style={{ position: 'absolute', top: 16, left: 16 }}>
Back
</button>
)}
/>| Prop | Type | Default | Description |
|---|---|---|---|
src |
string |
required | Image URL |
mode |
'preview' | 'edit' |
'preview' |
Interactive spots or position collector |
spots |
SpotDef[] |
[] |
Spot definitions |
onSpotPlace |
(pos, index) => void |
— | edit mode: fired on click |
enableImageSwap |
boolean |
true |
Enable image swap on hover/click |
swapDuration |
number |
400 |
Swap duration ms |
swapAnimation |
SwapAnimationPreset | SwapAnimationCustom |
'fade' |
Global swap animation |
hideOthersOnHover |
boolean |
true |
Fade out other spots on hover |
hideHoveredSpot |
boolean |
false |
Also hide the hovered spot |
onSpotHover |
(spot) => void |
— | Fired on mouse enter |
onSpotClick |
(spot) => void |
— | Fired on click |
onSpotLeave |
(spot) => void |
— | Fired on mouse leave |
| Prop | Type | Default | Description |
|---|---|---|---|
scenes |
Record<string, SceneDef> |
required | Map of sceneId → scene |
initialScene |
string |
required | Starting scene ID |
transition |
'fade' | 'none' |
'fade' |
Scene transition |
transitionDuration |
number |
350 |
Duration ms |
swapAnimation |
SwapAnimationPreset | SwapAnimationCustom |
'fade' |
Spot swap animation |
hideOthersOnHover |
boolean |
false |
Fade out other spots on hover |
renderBackButton |
({ goBack, canGoBack, history }) => ReactNode |
— | Custom back button |
showBackButton |
boolean |
true |
Show built-in back button |
backButtonLabel |
string |
'← Back' |
Built-in back button label |
showBreadcrumb |
boolean |
true |
Show scene breadcrumb |
onSceneChange |
(id, scene) => void |
— | Fired on scene change |
onSpotClick |
(spot, sceneId) => void |
— | Fired when spot is clicked |
| Prop | Type | Default | Description |
|---|---|---|---|
id |
string |
required | Unique identifier |
position |
{ x: number, y: number } |
required | Position in % (0–100) |
size |
{ w: number, h: number } |
{ w: 5, h: 5 } |
Hitbox size in % |
render |
(props: SpotRenderProps) => ReactNode |
required | Your render function |
hoverSrc |
string |
— | Image URL to swap on hover |
activeSrc |
string |
— | Image URL to swap on click |
swapAnimation |
SwapAnimationPreset | SwapAnimationCustom |
— | Per-spot animation override |
interface SpotRenderProps {
isHovered: boolean;
isActive: boolean;
onMouseEnter: (e: MouseEvent) => void;
onMouseLeave: (e: MouseEvent) => void;
onClick: (e: MouseEvent) => void;
goTo: (sceneId: string) => void; // SceneChain only
goBack: () => void; // SceneChain only
canGoBack: boolean; // SceneChain only
}interface SwapAnimationCustom {
enter: string; // e.g. "my-anim 400ms ease forwards"
leave?: string;
duration?: number; // Default: 400
}MIT
