dash-model-viewer
dash-model-viewer — interactive 3D models and AR for Dash
Google's <model-viewer> as a first-class component for Plotly Dash 4.
The web component ships inside the wheel · every interaction arrives as an ordinary Dash prop · hotspots are components, not dictionaries · unknown attributes pass through untouched · no clientside_callback required for anything.
Documentation · Discord · YouTube · GitHub
Live at modelviewer.2plot.dev — every model on the docs site is a running Dash app.
Maintained by Pip Install Python LLC.
Overview
<model-viewer> is a custom element, not a React component. Wrapping it for Dash means
answering two questions honestly: how does the runtime get onto the page, and how does a
DOM event become a Python prop?
Version 0.0.1 answered the first with a hard-coded CDN <script> injected into
document.body by every component instance, and the second not at all — its setProps
call was commented out, so the component had no output props. 1.0.0 answers both:
| Upstream shape | What Python sees | Why |
|---|---|---|
| A custom element defined by a script that must run before Dash mounts | The bundle vendored in the wheel, emitted through dash.hooks.script() as a classic script |
Classic scripts execute in document order, before the inline {%renderer%} statement that mounts the app. A type="module" or async resource defers past it and the element is undefined at mount — an intermittent "sometimes the model doesn't render". |
DOM events (camera-change, load, ar-status, …) |
Output props — camera, model_state, model_info, ar_status, ar_tracking, scene_point |
An event listener cannot be serialized. The state it carries can. camera is debounced because camera-change fires at frame rate. |
Named slots (hotspot-*, ar-button, poster) |
Slot — a component holding arbitrary Dash children |
dash.html.Div has no slot prop, which is the only reason the old wrapper needed hotspots to be dictionaries. Slots take real components now. |
| A kebab-case attribute surface that keeps growing upstream | mv_* props and an attributes dict |
Named props cannot cover an upstream that keeps growing. These two escape hatches mean a new <model-viewer> attribute needs no release of this package. |
The result is that a Dash developer writes ordinary @callbacks and never touches
JavaScript. The camera-views example went
from 230 lines to 47 in the rewrite, and lost its clientside layer entirely.
Installation
pip install dash-model-viewer
1.0.0 is not on PyPI yet. The published release is still
0.0.1— the version described under Upgrading below, with the CDN dependency and no output props. Until 1.0.0 ships, install from source:pip install git+https://github.com/pip-install-python/dash-model-viewerEverything documented here describes 1.0.0. Delete this note when the release is published.
Nothing else is required. The @google/model-viewer bundle (4.3.1, ~1 MB) ships inside the
wheel, so there is no CDN request at load time, no external_scripts entry to add, and no
build step for consumers. It works offline, behind a corporate egress proxy, and under a
strict script-src Content-Security-Policy — and the version is pinned by your lockfile
rather than by whatever a CDN is serving today.
Quick Start
from dash import Dash, html
import dash_model_viewer as dmv
app = Dash(__name__)
app.layout = html.Div([
dmv.ModelViewer(
id="viewer",
src="/assets/astronaut.glb",
alt="A 3D model of an astronaut",
camera_controls=True,
style={"width": "100%", "height": "480px"},
)
])
if __name__ == "__main__":
app.run(debug=True)
Importing the package is all the setup there is — the runtime is injected by a Dash hook at import time. AR is on by default and, as of 1.0.0, actually works.
Reading the model back is an ordinary callback:
from dash import Input, Output, callback
@callback(Output("readout", "children"), Input("viewer", "camera"))
def show_camera(camera):
if not camera:
return "Drag the model."
return f"orbit {camera['orbit']} · fov {camera['field_of_view']}"
Documentation
📚 modelviewer.2plot.dev
Thirteen pages, each one a running Dash app you can drag: quick start, attributes and parity, events and callbacks, camera and views, slots and hotspots, augmented reality, model switching, image-to-3D, generative 3D, a scene director, benchmarks, the full API reference, and a prop-by-prop migration guide.
Append /llms.txt to any page URL for the machine-readable Markdown of that page — the
whole site is built to be read by agents as well as people.
To run the docs site locally:
pip install -r requirements.txt
# markdown2dash pins gunicorn<22, against the CVE-driven gunicorn>=23 floor in
# requirements.txt. pip cannot resolve both, so it installs without its
# dependency graph — every one of its real dependencies is already pinned there.
pip install --no-deps markdown2dash==0.1.2
pip install . # the site documents the package in THIS checkout
python run.py
The prop surface
32 props on ModelViewer, 8 on Slot. Grouped by what they're for:
Source and framing
| Prop | Type | Notes |
|---|---|---|
src |
str |
Path or absolute URL to a .glb / .gltf |
alt |
str |
Set this. It is the accessible name of an otherwise opaque canvas |
poster |
str |
Shown until the model is interactive |
style, class_name |
dict / str |
The element is display: block with no intrinsic size — give it a height |
Camera
camera_controls · touch_action · camera_orbit · camera_target · field_of_view ·
min_field_of_view · max_field_of_view · min_camera_orbit · max_camera_orbit ·
interpolation_decay
Rendering and AR
ar · ar_modes · ar_scale · tone_mapping · shadow_intensity · variant_name
Output props — read-only from Python
Written by the component via setProps. Each is an ordinary callback Input.
| Prop | Updates when |
|---|---|
camera |
the user moves the camera (debounced; programmatic moves are suppressed) |
model_state |
loading progress, load success, load failure |
model_info |
on load — real dimensions in metres, GLTF variants, animation names |
ar_status / ar_tracking |
an AR session starts, places, fails, or loses tracking |
scene_point |
the user clicks the model, when pick_on_click=True |
Escape hatches
| Prop | Shape | Example |
|---|---|---|
mv_* |
mv_<snake_case> → <kebab-case> |
mv_environment_image="neutral" → environment-image="neutral" |
attributes |
raw dict, kebab-case keys |
{"orientation": "0deg 0deg 15deg", "exposure": "1.2"} |
Precedence when the same attribute is set twice: named prop > mv_* > attributes.
Slot
slot · position · normal · children · n_clicks · style · class_name
import dash_mantine_components as dmc
dmv.ModelViewer(
id="viewer", src="/assets/shoe.glb", alt="A running shoe",
children=[
dmv.Slot(id="sole", slot="hotspot-sole",
position="0 0.05 0.1", normal="0 1 0",
children=dmc.Badge("Carbon plate", color="teal")),
dmv.Slot(slot="ar-button",
children=dmc.Button("View in your space")),
],
)
n_clicks makes a hotspot a callback input like any dmc.Button.
Serving the bundle elsewhere
The vendored bundle is served by your own app. To use a public CDN or an internal mirror instead:
import dash_model_viewer as dmv
dmv.configure(use_cdn=True) # public jsDelivr
dmv.configure(use_cdn="https://cdn.example/mv.js") # your mirror
app = Dash(__name__)
Call it at module scope, before Dash() is constructed — the hook that emits the
script fires during app construction, so a later call has nothing left to change.
Dash compatibility
| Dash | 4.1 – 4.4 (dash.hooks.script is what the architecture rests on) |
| Python | 3.9 – 3.13 |
@google/model-viewer |
4.3.1, pinned exactly and bundled |
The wheel depends on Dash and nothing else. The documentation site's requirements are
separate and never reach a pip install dash-model-viewer.
Upgrading from 0.0.1
1.0.0 is a clean break: snake_case props, DashModelViewer → ModelViewer, and hotspot
dictionaries → Slot components. The full prop-by-prop table is in
CHANGELOG.md, and /migrating
walks it with runnable examples.
Three defects are fixed along the way, and they are the reason the break was worth it:
- AR works out of the box on Android.
ar_modesdefaulted to"basic_annotations scene-viewer quick-look".basic_annotationsis not an AR mode — it was a folder name inusage_tests/pasted into the default — sowebxrwas absent from every default configuration and the flagship feature had never worked without the user discovering and overriding the prop. The default is now"webxr scene-viewer quick-look". - Events reach Python.
setPropswas commented out, so the component had no output props at all and every interaction needed aclientside_callback. - Listeners no longer accumulate.
removeEventListenerwas called with a freshly created closure on every render, so it removed nothing.
Common gotchas
- Give the element a height.
<model-viewer>isdisplay: blockwith no intrinsic size. Without a height it renders at zero pixels and looks like a load failure. camera_change_debounceshould stay non-zero. It defaults to 100 ms.camera-changefires at frame rate, so0means one server callback per frame, per viewer, per user.altis not optional in practice. It is the accessible name for a canvas that screen readers and agents cannot otherwise describe.configure()must run beforeDash(). After construction the script resource is already emitted.- Don't reach for
clientside_callbackout of habit. If you are writing one to read camera state or hotspot clicks, there is a prop for it.
Development
There is no build step. No package.json, no webpack, no babel, no
dash-generate-components, no metadata.json — three hand-authored layers and zero
generated ones:
dash_model_viewer/vendor/model-viewer-umd.min.js Google's UMD build, 4.3.1, verbatim
dash_model_viewer/dash_model_viewer.js the shim — registers the namespace
dash_model_viewer/_components.py hand-written Component subclasses
pip install -e ".[dev]"
pytest -q
.claude/ARCHITECTURE.md holds the design record: why vendoring is a supply-chain fix
rather than a preference, why the script must be classic rather than a module, and what
each layer is responsible for.
Community & support
More from Pip Install Python LLC
Part of the 2plot network — component documentation sites, each one a running Dash app: leaflet · pannellum · excalidraw · muicharts · flexlayout · emojimart · flows · email · scheduler · llms · boilerplate
License
Apache-2.0 — see LICENSE.
@google/model-viewer is also Apache-2.0, by the Google model-viewer team. Its licence
ships alongside the bundle in dash_model_viewer/vendor/model-viewer-LICENSE.
Metadata
Release files for dash-model-viewer 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| dash_model_viewer-1.0.0.tar.gz | 512.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| dash_model_viewer-1.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 831.9 kB
Release files / dash_model_viewer-1.0.0.tar.gz
| Download URL | dash_model_viewer-1.0.0.tar.gz |
|---|---|
| Size | 512.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
beae3fd783d88e1a23732f5b141971d9c68f00c9a99d4e6a23ba178ff37f2e00
|
|
BLAKE2b-256 checksum How to use checksums |
04555eebde0c3667aa56d989d570db5763cbd503934d4134d729d8e747b973af
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 21, 2026.
Transparency logRelease files / dash_model_viewer-1.0.0-py3-none-any.whl
| Download URL | dash_model_viewer-1.0.0-py3-none-any.whl |
|---|---|
| Size | 319.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e5345afa41851b8a64d7f56af54e2a9ade7054766d710499dcd0d4372756bb43
|
|
BLAKE2b-256 checksum How to use checksums |
16bf6623acc18debe28d49dc1fb0a01efefb553a3d24cbe220916d55d24ecdeb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 21, 2026.
Transparency log