Reach every model-viewer attribute — including ones added upstream after this release — without waiting for a new version of this package.

Attributes and Parity

Reach every model-viewer attribute — including ones added upstream after this release — without waiting for a new version of this package.


The problem this solves

<model-viewer> has roughly seventy attributes and gains more with every release. A wrapper that hand-lists them is out of date the day upstream ships a new one, and every addition costs a regeneration, a release, and an upgrade on your side.

dash-model-viewer names about twenty of them and gives you two escape hatches that reach all of the rest — including attributes that do not exist yet.

FamilyLooks likeUse it for
Named propscamera_controls=TrueThe common ones. Validated, documented, autocompleted.
mv_* wildcardsmv_environment_image="neutral"Anything else, one at a time, at the call site.
attributes dictattributes={"exposure": "1.2"}Anything else, as data — from a callback, a config file, or a model.

Precedence when the same attribute is set twice: named prop > mv_* > attributes.


Live

Every attribute driven by this control is one dash-model-viewer has no named prop for. Nothing was regenerated to make them work.

# File: docs/attributes-and-parity/parity.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 CANDLE_GLASS, MOON_HDR

# Not one of these is a named prop on ModelViewer. They work anyway.
PRESETS = {
    "Studio": {"environment-image": "neutral", "exposure": "1"},
    "Moonlight": {"environment-image": MOON_HDR, "exposure": "1.4"},
    "Spinning": {"auto-rotate": "", "auto-rotate-delay": "0", "rotation-per-second": "20deg"},
    "Tilted": {"orientation": "0deg 0deg 20deg", "exposure": "1.1"},
}

component = html.Div(
    [
        dmc.SegmentedControl(
            id="ap-preset",
            data=list(PRESETS),
            value="Studio",
            fullWidth=True,
            mb="sm",
        ),
        dmv.ModelViewer(
            id="ap-viewer",
            # Glass, not the astronaut: this page is about lighting attributes,
            # and physically-based transparency and refraction respond to
            # `environment-image` and `exposure` far more visibly than a matte
            # spacesuit does. No camera props here — model-viewer auto-frames,
            # so swapping the model needs no hand-tuned orbit.
            src=CANDLE_GLASS,
            alt="A glass candle holder re-lit by attributes the package has no named prop for",
            shadow_intensity=1,
            style={"width": "100%", "height": "380px"},
        ),
        dmc.Code(id="ap-readout", block=True, mt="sm"),
    ]
)


@callback(
    Output("ap-viewer", "attributes"),
    Output("ap-readout", "children"),
    Input("ap-preset", "value"),
)
def apply_preset(name):
    attrs = PRESETS[name]
    rendered = "\n".join(f'{k}="{v}"' for k, v in attrs.items())
    return attrs, rendered

mv_* — the ergonomic form

Python snake_case becomes kebab-case attributes:

dmv.ModelViewer(
    id="viewer", src=..., alt=...,
    mv_environment_image="neutral",     # environment-image="neutral"
    mv_auto_rotate_delay="0",           # auto-rotate-delay="0"
    mv_disable_tap="",                  # disable-tap  (bare attribute)
)

Pass "" for boolean-style attributes whose meaning is presence, not value.


attributes — the data form

Use this when the attribute set is computed rather than typed:

@callback(Output("viewer", "attributes"), Input("theme", "value"))
def relight(theme):
    return {
        "environment-image": "neutral" if theme == "light" else MOON_HDR,
        "exposure": "1.0" if theme == "light" else "1.4",
        "shadow-softness": "0.8",
    }

Because it is an ordinary dict, it can come from anywhere — a database, a user preference, a JSON config, or a language model's structured output. That last one is the interesting case: the output space of "configure this viewer" is the entire <model-viewer> attribute surface, forever, with no allow-list to maintain.

The shim diffs each render against the previously applied set and removes anything that has gone away. Returning a dict without a key you previously set removes that attribute — you do not need a sentinel value. It also means the shim never re-writes an attribute that has not changed, so model-viewer is never interrupted mid-animation.


When to ask for a named prop instead

The escape hatches are complete, not equal. A named prop earns its place when the attribute:

unknown attributes);

If you find yourself writing the same attributes entry in every project, that is a good argument for a named prop — open an issue.


The full attribute list

Upstream, and always current: modelviewer.dev/docs. This package vendors model-viewer 4.3.1; dmv.MODEL_VIEWER_VERSION reports it at runtime, so you can check the docs against what you are actually running.


Source: /attributes-and-parity

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: