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.
| Family | Looks like | Use it for |
|---|---|---|
| Named props | camera_controls=True | The common ones. Validated, documented, autocompleted. |
mv_* wildcards | mv_environment_image="neutral" | Anything else, one at a time, at the call site. |
attributes dict | attributes={"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:
- needs validation (a typo in
attributesis silent — the browser ignores
unknown attributes);
- has a non-obvious default worth documenting;
- is two-way, like
camera_orbit; - or needs a Python-side type that is not a string.
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:
- /attributes-and-parity/llms.txt — LLM-friendly documentation
- /sitemap.xml
- /robots.txt