Skip to main content
dash-model-viewer 2plot.ai

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.

PyPI version Python Dash 4.x model-viewer 4.3.1 License: Apache-2.0 Discord YouTube

Documentation · Discord · YouTube · GitHub


dash-model-viewer running live at modelviewer.2plot.dev

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-viewer

Everything 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']}"

A dash-model-viewer scene being dragged, lit and switched between material variants

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_modes defaulted to "basic_annotations scene-viewer quick-look". basic_annotations is not an AR mode — it was a folder name in usage_tests/ pasted into the default — so webxr was 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. setProps was commented out, so the component had no output props at all and every interaction needed a clientside_callback.
  • Listeners no longer accumulate. removeEventListener was called with a freshly created closure on every render, so it removed nothing.

Common gotchas

  • Give the element a height. <model-viewer> is display: block with no intrinsic size. Without a height it renders at zero pixels and looks like a load failure.
  • camera_change_debounce should stay non-zero. It defaults to 100 ms. camera-change fires at frame rate, so 0 means one server callback per frame, per viewer, per user.
  • alt is not optional in practice. It is the accessible name for a canvas that screen readers and agents cannot otherwise describe.
  • configure() must run before Dash(). After construction the script resource is already emitted.
  • Don't reach for clientside_callback out 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)

Source distribution for dash-model-viewer 1.0.0
File Size Uploaded
dash_model_viewer-1.0.0.tar.gz 512.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for dash-model-viewer 1.0.0
File Interpreter ABI Platform
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 log

Release 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

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files

0.0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page