Install the package, put a 3D model on the page, and understand what got injected.

Quick Start

Install the package, put a 3D model on the page, and understand what got injected.


Install

pip install dash-model-viewer

One dependency: dash>=4.1. Google's model-viewer bundle is inside the wheel — there is no npm step, no CDN request at runtime, and nothing to add to external_scripts.


Your first viewer

Two props are required — src and alt — and one is strongly advised: style, because the element has no intrinsic size and will collapse to nothing without a height.

# File: docs/quick-start/basic_viewer.py

from dash import html

import dash_model_viewer as dmv
from lib.demo_models import ASTRONAUT

component = html.Div(
    dmv.ModelViewer(
        id="qs-viewer",
        src=ASTRONAUT,
        alt="A 3D model of an astronaut in a white spacesuit",
        style={"width": "100%", "height": "420px"},
    )
)

Drag to orbit. Scroll to zoom. On a phone, the AR button in the corner opens the model in your room.


What just happened

Importing dash_model_viewer registers a Dash hook, which injects two scripts into every page:

ScriptSizeWhat it is
vendor/model-viewer-umd.min.js~1 MBGoogle's model-viewer 4.3.1, vendored verbatim
dash_model_viewer.js~13 KBThe Dash ↔ custom-element shim

Both are served by your server from the installed wheel, so the component works offline, behind a corporate egress proxy, and under a strict script-src Content-Security-Policy. The version is pinned by your lockfile rather than by whatever a CDN is serving today.

The bundle loads on every page that imports the package, eagerly and synchronously. That is the price of having the custom element defined before Dash mounts — see Why it loads that way. If your app has one 3D page out of thirty, import the package in that page module rather than in app.py.


Serving the bundle from elsewhere

To use a CDN or an internal mirror instead of the vendored copy:

import dash_model_viewer as dmv

dmv.configure(use_cdn=True)                         # public jsDelivr
dmv.configure(use_cdn="https://cdn.example/mv.js")  # your own mirror

app = Dash(__name__)

Call it at module scope. Dash reads the hook's resource list while generating the index page, so a configure() buried in a callback or a lazily-imported page module may or may not take effect depending on which request arrives first — which is worse than never working at all.


Why it loads that way

Not a detail, and worth knowing before you "optimise" it.

<model-viewer> is a custom element. If its definition has not executed by the time Dash mounts your layout, the browser renders an unknown, unsized element and you get a blank box — sometimes, depending on network timing. Version 0.0.1 fought this with customElements.whenDefined() and a runtime-injected <script type="module">.

The fix is ordering, not defensive code. Dash's index ends:

<footer>
    {%config%}
    {%scripts%}     <!-- every <script src>, hook scripts last -->
    {%renderer%}    <!-- inline: new DashRenderer(...) -->
</footer>

{%renderer%} is the inline statement that actually mounts the app. A classic script in {%scripts%} runs before the parser reaches it, so the element is always defined in time. A type="module" script is deferred until after document parse — i.e. after new DashRenderer(...) has already run — and the race comes straight back.

So the package vendors the UMD build, and never sets type, async or defer. All three are asserted by the test suite, because none of them fail loudly — they fail as "sometimes the model doesn't render, on someone else's machine".


Next

dimensions as ordinary Dash props.

broken until 1.0.0.


Source: /quick-start

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: