Hotspot
Headless image hotspots: positioned markers with popover details and SVG regions.
|
|
| Package |
@vizejs/ui/hotspot |
| Maturity |
stable |
| Own the source |
vize lib pull hotspot |
| Requires |
context, controllable-state, id, popover |
| Aliases |
hotspot, image map, annotated image, shoppable image, product tags |
| Covers |
HTML map/area, WAI-ARIA Disclosure, Popover API |
Minimal setup
Use a Vue 3.5+ project with Vue SFC compilation configured (see Vite+ integration).
vp install @vizejs/ui
Add the public import below to <script setup lang="ts">. Components provide behavior without styles; the preview uses optional Paper styles.
import { HotspotArea, HotspotContent, HotspotImage, HotspotMarker, Hotspot, HotspotRoot } from "@vizejs/ui/hotspot";
Or copy the source into your project with vize lib pull hotspot (see Source Distribution).
Try it
This is the basic example maintained with the component source, using the public package imports. The preview adds the optional Paper styles.
Open the interactive example · Styling these components
Copy the example
<!-- Product photo with labelled markers that open detail popovers and a clickable window region. -->
<script setup lang="ts">
import { ref } from "vue";
import { Hotspot, HotspotArea, HotspotContent, HotspotImage, HotspotMarker } from "@vizejs/ui/hotspot";
const active = ref<string | null>(null);
</script>
<template>
<Hotspot v-model:active="active">
<HotspotImage src="/media/living-room.jpg" alt="Furnished living room" />
<HotspotMarker id="lamp" :x="25" :y="40" label="Floor lamp">
<HotspotContent>Brass floor lamp with a linen shade.</HotspotContent>
</HotspotMarker>
<HotspotMarker id="sofa" :x="60" :y="70" label="Sofa">
<HotspotContent>Three-seat sofa in oat linen.</HotspotContent>
</HotspotMarker>
<HotspotArea
id="window"
label="Window"
:shape="{ type: 'rect', x: 70, y: 5, width: 25, height: 30 }"
/>
</Hotspot>
<output>{{ active ?? "No hotspot selected" }}</output>
</template>
API
HotspotArea
Source: src/families/media/hotspot/hotspot-area.vue
Props
| Prop |
Type |
Default |
Description |
id (required) |
string |
required |
Area id; activating the area makes it the root's active hotspot. |
shape (required) |
HotspotShape |
required |
Region in image-space percent. |
label (required) |
string |
required |
Accessible name of the region. |
href |
string |
undefined |
Render the region as a link to this URL instead of a toggle button. |
disabled |
boolean |
false |
Remove the region from interaction. |
Events
| Event |
Payload |
Description |
activate |
[nativeEvent: Event] |
Fired when a button region is activated, before the active hotspot toggles. |
Slots
| Slot |
Slot props |
Description |
default |
HotspotAreaSlotState |
Extra SVG content drawn inside the region, e.g. a <title>. |
Exposed
| Member |
Type |
Description |
id |
string |
Area id. |
active |
boolean |
Whether this area is the active hotspot. |
element |
SVGSVGElement | null |
Rendered SVG overlay. |
shape |
HotspotShape |
Area shape. |
HotspotContent
Source: src/families/media/hotspot/hotspot-content.vue
Props
| Prop |
Type |
Default |
Description |
placement |
PopoverPlacement |
"top" |
Preferred placement relative to the marker before collision handling. |
offset |
number |
8 |
Gap between marker and content in CSS pixels. |
portalDisabled |
boolean |
false |
Render in place instead of portalling to document.body. |
closeOnEscape |
boolean |
true |
Close when Escape is pressed while the content or marker has focus. |
closeOnPointerDownOutside |
boolean |
true |
Close on pointer down outside the content and marker. |
closeOnFocusOutside |
boolean |
false |
Close when focus moves outside. Off by default so neighbouring markers, and focus restored from another marker's content, never close this one. |
ariaLabel |
string |
undefined |
Accessible dialog name when no labelled element is referenced. |
ariaLabelledby |
string |
undefined |
Space-separated ids that label the content. |
Slots
| Slot |
Slot props |
Description |
default |
HotspotMarkerSlotState |
Hotspot details. Receives the marker state. |
HotspotImage
Source: src/families/media/hotspot/hotspot-image.vue
Props
| Prop |
Type |
Default |
Description |
src (required) |
string |
required |
Image source. Unsafe or malformed sources are not rendered. |
alt (required) |
string |
required |
Alternative text describing the whole image. |
width |
number | string |
undefined |
Intrinsic width reserving layout space. |
height |
number | string |
undefined |
Intrinsic height reserving layout space. |
loading |
"eager" | "lazy" |
"lazy" |
Native loading policy. |
Exposed
| Member |
Type |
Description |
element |
HTMLImageElement | null |
Rendered native image. |
HotspotMarker
Source: src/families/media/hotspot/hotspot-marker.vue
Props
| Prop |
Type |
Default |
Description |
id (required) |
string |
required |
Marker id, unique within the root. |
x (required) |
number |
required |
Horizontal position in percent of the image width. |
y (required) |
number |
required |
Vertical position in percent of the image height. |
label (required) |
string |
required |
Accessible name of the marker button. |
disabled |
boolean |
false |
Remove the marker from interaction while keeping it rendered. |
Slots
| Slot |
Slot props |
Description |
trigger |
HotspotMarkerSlotState |
Visual marker contents rendered inside the button. |
default |
HotspotMarkerSlotState |
HotspotContent for this marker. |
Exposed
| Member |
Type |
Description |
id |
string |
Marker id. |
open |
boolean |
Whether the marker content is open. |
disabled |
boolean |
Whether the marker is disabled. |
state |
HotspotMarkerState |
Stable state token. |
element |
HTMLDivElement | null |
Rendered marker wrapper. |
focus |
(options?: FocusOptions) => void |
Focus the marker button. |
Hotspot<Data = undefined>
Source: src/families/media/hotspot/hotspot-root.vue
Props
| Prop |
Type |
Default |
Description |
id |
string | null |
undefined |
Consumer-owned base id. null and undefined select a deterministic fallback. |
hotspots |
readonly HotspotDefinition<Data>[] |
[] |
Declared markers. Their data type flows into the default slot so markers can be rendered with v-for while keeping consumer payloads typed. |
active |
string | null |
undefined |
Controlled active marker id (v-model:active). undefined selects uncontrolled behavior. |
defaultActive |
string | null |
null |
Initially active marker for uncontrolled use. |
exclusive |
boolean |
true |
Keep at most one marker open. When false, markers open independently and active tracks the most recently opened one. |
Events
| Event |
Payload |
Description |
update:active |
[id: string | null] |
Fired when the active marker requests a new controlled value. |
activeChange |
[id: string | null, previous: string | null, source: HotspotActiveChangeSource] |
Fired after every distinct active-marker change with its source. |
Slots
| Slot |
Slot props |
Description |
default |
HotspotSlotState<Data> |
Image, markers, areas, and content. Receives the typed hotspot list and open state. |
Exposed
| Member |
Type |
Description |
active |
— |
|
close |
— |
|
element |
Readonly<ShallowRef<HTMLElement | null>> |
Template ref to the rendered root element. |
hitTest |
— |
|
open |
— |
|
openIds |
— |
|
HotspotRoot<Data = undefined>
Source: src/families/media/hotspot/hotspot-root.vue
Props
| Prop |
Type |
Default |
Description |
id |
string | null |
undefined |
Consumer-owned base id. null and undefined select a deterministic fallback. |
hotspots |
readonly HotspotDefinition<Data>[] |
[] |
Declared markers. Their data type flows into the default slot so markers can be rendered with v-for while keeping consumer payloads typed. |
active |
string | null |
undefined |
Controlled active marker id (v-model:active). undefined selects uncontrolled behavior. |
defaultActive |
string | null |
null |
Initially active marker for uncontrolled use. |
exclusive |
boolean |
true |
Keep at most one marker open. When false, markers open independently and active tracks the most recently opened one. |
Events
| Event |
Payload |
Description |
update:active |
[id: string | null] |
Fired when the active marker requests a new controlled value. |
activeChange |
[id: string | null, previous: string | null, source: HotspotActiveChangeSource] |
Fired after every distinct active-marker change with its source. |
Slots
| Slot |
Slot props |
Description |
default |
HotspotSlotState<Data> |
Image, markers, areas, and content. Receives the typed hotspot list and open state. |
Exposed
| Member |
Type |
Description |
element |
HTMLDivElement | null |
Rendered root element. |
active |
string | null |
Most recently opened marker, or null. |
openIds |
readonly string[] |
Ids of every open marker. |
open |
(id: string) => boolean |
Open one marker (closing others unless exclusive is false). Reports whether state changed. |
close |
(id?: string) => boolean |
Close one marker, or every marker without an id. Reports whether state changed. |
hitTest |
(x: number, y: number) => readonly string[] |
Ids of registered HotspotAreas whose shape contains an image-space point (percent). |
Behavior
Normative state x input -> outcome table for hotspot-root.vue,
hotspot-image.vue, hotspot-marker.vue, hotspot-content.vue, and
hotspot-area.vue (@vizejs/ui/hotspot). Every row is proven by the named test.
Markers are positioned in image space: each HotspotMarker publishes
--vize-ui-hotspot-x / --vize-ui-hotspot-y percentages, so a consumer rule
like position: absolute; left: var(--vize-ui-hotspot-x); top: var(--vize-ui-hotspot-y)
keeps markers attached to the image at every size. Each marker is a Popover
(PopoverRoot + PopoverTrigger button), and HotspotContent wraps
PopoverContent, inheriting Escape, outside dismissal, positioning, and focus
return. HotspotArea draws a region on a viewBox="0 0 100 100" SVG overlay.
| ID |
State |
Input |
Outcome |
Evidence |
| H1 |
default |
render |
markers render as named buttons with position variables and states; the image keeps safe src/alt; disabled markers are disabled buttons |
renders positioned marker buttons, a safe image, regions, and typed slot data |
| H2 |
exclusive (default) |
marker click |
opens that marker's content, closes the previous one, toggles closed on a second click; emits update:active and activeChange |
clicking a marker opens its content and exclusive roots close the previous one |
| H3 |
open content |
Escape |
closes the content and returns focus to its marker |
Escape closes the open content and returns focus to its marker |
| H4 |
exclusive=false |
clicks / open() / close() |
several markers stay open; active is the latest; closing it restores the previous one; close() without an id closes all |
non-exclusive roots keep several markers open and track the latest as active |
| H5 |
focused marker |
Arrow keys |
focus moves to the nearest enabled marker inside a 90° cone in that direction; modified keys and dead ends pass through |
arrow keys move focus to the spatially nearest enabled marker |
| H6 |
controlled active |
click / prop change |
emits the request while the controlled marker stays open until the parent accepts it |
controlled active wins until the parent accepts the request |
| H7 |
regions |
click / Enter / hit test |
button regions toggle with aria-pressed; link regions render sanitized href; hitTest() returns areas containing a point |
regions toggle as buttons, render safe links, and support hit testing |
| H8 |
unsafe / disabled |
render / click |
unsafe image sources and links are dropped; disabled regions leave the tab order and ignore activation |
unsafe links and images are not rendered and disabled regions stay inert |
| H9 |
region listener |
activate + preventDefault() |
the toggle is cancelled |
region activate listeners can cancel the toggle |
| H10 |
missing provider |
setup |
markers and areas require HotspotRoot; content requires HotspotMarker |
compound parts require matching providers |
| H11 |
pure geometry |
helpers |
even-odd polygon test, inclusive rect/circle tests, point formatting, clamping, directional search, and href sanitizing are deterministic |
hotspot-geometry.test.ts |
| H12 |
SSR |
isolated requests |
markup is byte-identical, including generated marker ids, the open default marker, and the SVG region |
renders byte-identical hotspot markup across isolated SSR requests |
| H13 |
SSR / hydration |
hydrate |
server markup hydrates without warnings or node replacement |
hydrates hotspot markup without warnings or node replacement |
| H14 |
types |
compile |
hotspot payloads flow through HotspotSlotState<Data>; states, shapes, and exposes are closed and read-only |
hotspot.types.test-d.ts |
HotspotContent defaults closeOnFocusOutside to false so that focus returning
to one marker never closes another open marker.