Slots and Hotspots
Anchor any Dash component to a point on the model — and why hotspots used to be a list of dictionaries.
Anchored labels
A hotspot is a Slot whose name begins with hotspot- and which carries a position in model space. <model-viewer> projects it onto the model, keeps it attached as the camera moves, and hides it when the geometry occludes it.
# File: docs/slots-and-hotspots/hotspots.py
from dash import Input, Output, callback, html
import dash_mantine_components as dmc
import dash_model_viewer as dmv
from lib.demo_models import ASTRONAUT
component = html.Div(
[
dmv.ModelViewer(
id="sh-viewer",
src=ASTRONAUT,
alt="An astronaut with labelled hotspots on the visor and backpack",
camera_orbit="15deg 80deg 3.5m",
camera_target="0m 1.1m 0m",
style={"width": "100%", "height": "420px"},
children=[
dmv.Slot(
id="sh-visor",
slot="hotspot-visor",
position="0 1.75 0.35",
normal="0 0 1",
children=dmc.Badge("Visor", color="indigo", variant="filled"),
),
dmv.Slot(
id="sh-pack",
slot="hotspot-pack",
position="0 1.4 -0.35",
normal="0 0 -1",
children=dmc.Badge("Life support", color="teal", variant="filled"),
),
],
),
dmc.Text("Click a badge on the model.", id="sh-readout", size="sm", mt="sm"),
]
)
@callback(
Output("sh-readout", "children"),
Input("sh-visor", "n_clicks"),
Input("sh-pack", "n_clicks"),
)
def describe(visor, pack):
return f"Visor clicked {visor or 0}x · Life support clicked {pack or 0}x"
The children are a real dmc.Badge. Not a string, not a class name, not a config dict — an actual Dash component, with all of its props.
Why this was a list of dictionaries
In 0.0.1 hotspots looked like this:
DashModelViewer(
hotspots=[
{"slot": "hotspot-visor", "position": "0 1.75 0.35",
"text": "Visor", "children_classname": "label"},
],
)
Content was limited to a text string and a CSS class. That was not a design choice — it was a workaround.
<model-viewer> places its extras into named shadow-DOM slots, which requires a child element carrying a slot attribute. dash.html.Div has no slot prop, and neither does any other core Dash component. There was no way to put an arbitrary Dash component into a slot, so the component rendered its own plain <div>s from a serialisable description of them, and the description was the only thing a user could reach.
Slot is a component that does have slot. Everything downstream follows — arbitrary children, real callbacks, n_clicks.
Every slot, not just hotspots
Slot is the general mechanism. The AR button, the AR prompt, the failure message, the poster and the progress bar are all just named slots, so the three separate props 0.0.1 had for them (arButtonText, customArPrompt, customArFailure) collapse into one component:
dmv.ModelViewer(
id="viewer", src=..., alt=...,
children=[
dmv.Slot(slot="ar-button", children=dmc.Button("View in your space")),
dmv.Slot(slot="ar-prompt", children=dmc.Loader(size="sm")),
dmv.Slot(slot="ar-failure", children=dmc.Alert("AR lost tracking", color="red")),
dmv.Slot(slot="poster", children=html.Img(src="/assets/poster.webp")),
dmv.Slot(slot="progress-bar", children=html.Div(className="my-bar")),
],
)
The full list of slot names is in model-viewer's own documentation.
n_clicks is the point
Each Slot has its own n_clicks, so a hotspot is a callback Input like any button:
@callback(Output("viewer", "camera_orbit"), Input("sh-visor", "n_clicks"))
def look_at_visor(_):
return "0deg 70deg 1.2m"
For many hotspots, use pattern-matching ids — see Camera and views, which drives four presets from one callback.
Styling
Slots render with the class dmv-slot, plus anything you pass as class_name. They are ordinary DOM, so ordinary CSS reaches them:
.dmv-slot {
--min-hotspot-opacity: 0; /* fade out when occluded */
}
.dmv-slot[slot^="hotspot-"] {
background: rgba(0, 0, 0, .65);
border-radius: 999px;
padding: 4px 10px;
color: white;
cursor: pointer;
}
position="0 1.75 0.35" is metres in the model's own coordinate system, so the numbers that work for one model are meaningless for another. To place hotspots on a model you have not measured, set pick_on_click=True and read the coordinates back from scene_point — see Events and callbacks.
Source: /slots-and-hotspots
Note for AI agents: This is the static, prerendered view of an interactive Dash application served because we detected a non-JS user agent. Full prose docs:
- /slots-and-hotspots/llms.txt — LLM-friendly documentation
- /sitemap.xml
- /robots.txt