Skip to main content


Hotspots provide a way to show annotations or “points of interest” on top of the 3D product, which follow an X,Y,Z coordinate in the 3D scene and can be fully controlled and customized in your page’s HTML.

These hotspots are useful for showing additional information about a product, and can even contain interactive elements such as color swatches and buttons to configure that part of the product.

API reference

Hotspot example

Creating a new hotspot

Heads up

The Dopple app is still in beta but nearing release. Once released, you will be able to login and define hotspots for your products in the app’s dashboard. For now, Dopple’s team will work with you to define and position any hotspots needed.

Once a hotspot is created on your product within the Dopple app, you will be able to access its data on your site via the hotspot property on the product instance.

// Create the product instance when first initializing Dopple Visual on your page
const myProduct = new Atlatl.Product(template)

// Get the array of hotspots available on the product
const hotspotData = myProduct.hotspots

In this example above, the hotspotData array contains objects with the following properties:

  • name - A unique name/ID given to the hotspot.
  • position - The X,Y,Z coordinates of the hotspot on the user's screen (X and Y are horizontal and vertical offsets respectively; Z indicates depth and can be used for setting the element’s z-index).
  • visible - true if the hotspot is currently visible, or false if it is occluded by another mesh or out of view of the camera.
  • position3D - The X,Y,Z coordinates of the hotspot in the 3D scene.
// console.log(hotspotData) yields the following
"name": "foo",
"position": [123, 234, 345],
"visible": true,
"position3D": [0.2, -0.6, -0.3]
"name": "bar",
"position": [12, 456, 1234],
"visible": false,
"position3D": [0.1, 0.7, -0.4]
// etc...

For instructions on how to initialize the product before accessing its hotspots, see Creating the Product instance.

Adding content to your hotspots

The content for each hotspot (such as text, images, buttons, etc.) is defined in your page’s HTML, and will be positioned on top of your <canvas> or Visual Component.

<canvas id="dopple-visual"></canvas>
<div id="hotspot-foo">
<h2>My Hotspot #1</h2>
<p>This is some text that will be displayed on top of the 3D product.</p>

As a best practice, it is ideal to place the hotspots as sibling elements to the <canvas> element, and have a parent container element with position: relative; set on it. This will allow the hotspots to be positioned correctly on top of the 3D product during the final step later.

<div style="position: relative;">
<canvas id="dopple-visual"></canvas>
<!-- Hotspot #1 -->
<div id="hotspot-foo">
<h2>My Hotspot</h2>
<p>This is some text that will be displayed on top of the 3D product.</p>
<!-- Hotspot #2 -->
<div id="hotspot-bar">
<h2>My Hotspot #2</h2>
<p><img src="/path/to/some/image.png" /></p>
<!-- Hotspot #3 -->
<div id="hotspot-baz">
<h2>My Hotspot #3</h2>
<button>Click me</button>
<!-- etc... -->

Attaching hotspot elements to the 3D product

Each hotspot in the array of hotspots on the product comes with an attach() method. This method accepts two parameters:

  1. An HTML element (required)
  2. An options object (optional)

HTML element

The first parameter is an HTML element that will be “attached” to the hotspot and will be positioned and displayed without the need for any manual logic or calculations.

<div id="hotspot-foo">
<h2>My Hotspot</h2>
const hotspotElement = document.getElementById('hotspot-foo')

Once attached, the hotspot element will automatically update its left and top positions to follow the hotspot within the 3D scene as it rotates and moves.

Options object

The second parameter is an optional options object that can be used to customize the behavior of the hotspot element.

const options = {
autoPosition: true,
autoShow: false

myProduct.hotspots[0].attach(hotspotElement, options)
  • autoPosition (boolean) have the attached element’s transform property automatically updated to match the hotspot’s position.
  • autoShow (boolean) have the attached element automatically shown when the hotspot is visible, and hidden when the hotspot is not visible.

Listening for the visibilitychange event

An element that is attached to a hotspot can listen to the visibilitychange event (by using addEventListener) to detect when the hotspot’s visible property has been toggled. This property will be true when the hotspot is in view of the camera, and false when it is either occluded by another mesh or out of view of the camera.

This is especially useful for adding or removing a visibility class on the hotspot element once the hotspot goes out of view.

hotspot.addEventListener('visibilitychange', (event) => {
if (event.detail.visible) {
} else {

Building a full hotspot component

Using the concepts above, we can create a hotspot component for a real world use case:

  • Create a stylized dot for the hotspot itself.
  • Create a content box that pops up when the dot is clicked.
  • Hide the hotspots until the 3D product has finished loading.

Here is an example of the full hotspot component in action (click the dot to see the hotspot content pop up):


I’m a hotspot!

The component’s markup

To help ensure the hotspot and its content are fully accessible, we will use a hidden checkbox input to control whether or not the hotspot content is currently visible, and control the appearance of the hotspot’s dot.

The main hotspot component itself will wrap everything the dot, the hotspot content, and the hidden checkbox controlling the hotspot content inside of a containing <div>:

<div class="hotspot" id="hotspot-example">
<input id="hotspot-checkbox" type="checkbox" />
<label class="hotspot-dot" for="hotspot-checkbox">
<svg viewBox="0 0 24 24">
<line x1="12" y1="8" x2="12" y2="16" />
<line x1="8" y1="12" x2="16" y2="12" />
<div class="hotspot-content">
<p>I’m a hotspot!</p>

By using a <label> for the hotspot dot with a for attribute matching the id on the checkbox input, the dot will toggle the checkbox when clicked.

Attaching the component to the product

After initializing the product on your page, use the hotspot’s attach() method to automatically attach the hotspot element to the hotspot’s position:

const hotspotElement = document.getElementById('hotspot-example')


hotspotElement.addEventListener('visibilitychange', (event) => {
if (event.detail.visible) {
} else {

Showing the component once the product has loaded

To prevent users from accidentally seeing hotspots or interacting with their content before the product is ready, we can hide any hotspots while the page is still loading, then show them once the 3D product is ready.

// Hide the hotspot component(s) while the page loads
document.querySelectorAll('.hotspot').forEach((hotspot) => { = 'none'

// Initialize Dopple Visual and load the product once the document is ready
window.addEventListener('load', async () => {

// *** Include any initial logic for loading your product here first ***

// Wait until the product is fully loaded and ready
await myProduct.ready()

// Hide the loading screen

// Show the hotspot component(s) once the product is ready
document.querySelectorAll('.hotspot').forEach((hotspot) => {

For full instructions on how to initialize Dopple Visual on your page and load your product, see Creating and initializing Dopple Visual.

Styling the component

More detailed explanations are included in the comments below, but the main idea is to visually hide the hotspot’s checkbox (while still keeping it accessible to keyboard users), then style the dot and hotspot content according to the checkbox’s checked state.

/* The container element for the hotspot component */
.hotspot {
height: 0;
position: relative;
transition: all 33ms ease; /* Add a slight bit of easing to reduce jittery motion */
width: 0;
z-index: 2; /* Give the hotspot a z-index greater than the canvas' z-index */

/* Visually hide the checkbox input while keeping it keyboard accessible */
.hotspot input[type='checkbox'] {
border: 0 !important;
clip: rect(0, 0, 0, 0) !important;
height: 1px !important;
margin: -1px !important;
overflow: hidden !important;
padding: 0 !important;
position: absolute !important;
white-space: nowrap !important;
width: 1px !important;

/* The visible dot for the hotspot */
.hotspot-dot {
align-items: center;
background: #007BEE;
border-radius: 1rem;
box-shadow: 0 0.125rem 0.5rem rgba(0, 0, 0, 0.25);
color: #FFF;
content: "+"; /* Optional: show a character such as a + or • within the dot */
cursor: pointer;
display: flex;
height: 1.5rem;
justify-content: center;
margin: 0;
padding: 0;
position: absolute;
transform: translate(-50%, -50%); /* Center the dot within the hotspot */
transition: all 300ms cubic-bezier(0.06, 0.8, 0.2, 1);
user-select: none;
width: 1.5rem;

/* Enlarge the hotspot when hovered or focused */
.hotspot input[type='checkbox']:focus-visible ~ .hotspot-dot {
transform: translate(-50%, -50%) scale(1.15);

/* Give the hotspot a subtle outline when focused */
.hotspot input[type='checkbox']:focus-visible ~ .hotspot-dot {
outline: 4px solid rgba(0, 128, 234, 0.25);

.hotspot-dot svg {
fill: none;
height: 1.5rem;
stroke: currentColor;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 2;
width: 1.5rem;

.hotspot-content {
background: #FFF;
border-radius: 0.625rem;
bottom: calc(100% + 1.5rem); /* Slightly offset the hotspot content from the dot */
box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.2);
left: 50%;
padding: 1rem;
pointer-events: none; /* Initially prevent any mouse events on the content when inactive */
position: absolute;
opacity: 0; /* Initially hide the hotspot when inactive */
transform: translate(-50%, 0) scale(0.75); /* Center the hotspot and initially shrink it when inactive */
transform-origin: bottom center;
transition: all 350ms cubic-bezier(0.06, 0.8, 0.2, 1);
visibility: hidden; /* Initially hide the hotspot when inactive */
width: 15rem;

/* Add the small triangle to the bottom of the hotspot content */
.hotspot-content::after {
border: solid 0.5rem transparent;
border-top-color: #FFF;
bottom: -0.875rem;
content: '';
display: block;
left: 50%;
position: absolute;
transform: translate(-50%, 0);

/* Show the hotspot content when active */
.hotspot input[type='checkbox']:checked ~ .hotspot-content {
opacity: 1;
pointer-events: auto;
transform: translate(-50%, 0);
visibility: visible;

/* Change the dot's background when active */
.hotspot input[type='checkbox']:checked ~ .hotspot-dot {
background: #EF5493;

Full code example

<html lang="en">
<meta charset="utf-8">
<title>My Page</title>
<!-- Link to the Visual Component's scripts -->
<script src="" defer></script>
<!-- Custom scripts -->
<script src="scripts.js" defer></script>
<!-- Custom styles -->
<link rel="stylesheet" href="styles.css" />
<div class="canvas-container">
<!-- Canvas to render the 3D scene to -->
<canvas id="my-canvas"></canvas>
<!-- Hotspot #1 -->
<div class="hotspot" id="hotspot-1">
<input id="hotspot-1-checkbox" type="checkbox" />
<label class="hotspot-dot" for="hotspot-1-checkbox">
<svg viewBox="0 0 24 24">
<line x1="12" y1="8" x2="12" y2="16" />
<line x1="8" y1="12" x2="16" y2="12" />
<div class="hotspot-content">
<p>First hotspot content.</p>
<!-- Hotspot #2 -->
<div class="hotspot" id="hotspot-2">
<input id="hotspot-2-checkbox" type="checkbox" />
<label class="hotspot-dot" for="hotspot-2-checkbox">
<svg viewBox="0 0 24 24">
<line x1="12" y1="8" x2="12" y2="16" />
<line x1="8" y1="12" x2="16" y2="12" />
<div class="hotspot-content">
<p>Second hotspot content.</p>