Animation
Play a model's built-in animation clips, switch between them with a crossfade, and pause — entirely through attributes, with the clip list reported by the model itself.
Clips come from the model, not from the page
A .glb can carry animation clips, and model_info["animations"] lists the ones it has. Robot Expressive carries fourteen — Dance, Death, Idle, Jump, No, Punch, Running, Sitting, Standing, ThumbsUp, Walking, WalkJump, Wave, Yes.
The dropdown is filled from that prop, so this page would work unchanged if you pointed src at a different animated model. A hardcoded list of those fourteen names would have looked identical today and broken silently the first time somebody swapped the model.
# File: docs/animation/animation.py
from dash import Input, Output, State, callback, html, no_update
import dash_mantine_components as dmc
import dash_model_viewer as dmv
from lib.demo_models import ROBOT
#: Lighting that makes a moving figure readable. A matte grey character against
#: a flat background reads as a bug report; this reads as a character.
BASE_ATTRS = {
"environment-image": "neutral",
"shadow-softness": "0.6",
}
#: Milliseconds of blend when switching clips. 0 is a hard cut and is the
#: default most people expect to be wrong — see the page.
DEFAULT_CROSSFADE = 300
component = html.Div(
[
dmc.Group(
[
dmc.Select(
id="an-name",
label="Animation",
# Filled from the model itself, not hardcoded — see below.
data=[],
w=220,
allowDeselect=False,
),
dmc.NumberInput(
id="an-crossfade",
label="Crossfade (ms)",
value=DEFAULT_CROSSFADE,
min=0,
max=2000,
step=100,
w=150,
),
dmc.Switch(
id="an-playing",
label="Playing",
checked=True,
mt="xl",
),
],
mb="sm",
align="flex-end",
),
dmv.ModelViewer(
id="an-viewer",
src=ROBOT,
alt="An expressive cartoon robot playing one of its built-in animation clips",
camera_controls=True,
shadow_intensity=1,
# `autoplay` starts the first clip. Without it the model loads in
# its bind pose and looks broken rather than idle.
attributes={**BASE_ATTRS, "autoplay": ""},
style={"width": "100%", "height": "460px"},
),
dmc.Text(id="an-status", size="sm", c="dimmed", mt="xs"),
]
)
@callback(
Output("an-name", "data"),
Output("an-name", "value"),
Output("an-status", "children"),
Input("an-viewer", "model_info"),
State("an-name", "value"),
)
def list_animations(info, current):
"""The viewer reports the clips; the page never hardcodes them.
`model_info["animations"]` is the same prop /model-switching uses for
material variants, and it arrives with the `load` event. So this picker
works for ANY model the src is pointed at — which is the point, and is
what a hardcoded list of Robot Expressive's fourteen clips would have
quietly broken the moment somebody changed the model.
"""
if not info:
return [], None, "Loading the model…"
clips = info.get("animations") or []
if not clips:
return [], None, "This model carries no animation clips."
chosen = current if current in clips else clips[0]
return clips, chosen, f"{len(clips)} clips: {', '.join(clips)}"
@callback(
Output("an-viewer", "attributes"),
Input("an-name", "value"),
Input("an-crossfade", "value"),
Input("an-playing", "checked"),
)
def drive_animation(name, crossfade, playing):
"""Everything here is an ATTRIBUTE, not a method call.
1.0.0 ships no imperative surface — no `play()`, no `pause()` — so the
whole of this page runs through the `attributes` escape hatch. That is the
honest demonstration of what the hatch is for.
`paused` is a boolean attribute: its PRESENCE pauses. So resuming means
leaving the key out of the dict entirely rather than setting it false, and
the shim removes any attribute that disappears between renders.
"""
if not name:
return no_update
attrs = {
**BASE_ATTRS,
"autoplay": "",
"animation-name": name,
"animation-crossfade-duration": str(crossfade or 0),
}
if not playing:
attrs["paused"] = ""
return attrs
It is the same prop that drives the variant dropdown on Model Switching — one read-only payload, two features.
Everything here is an attribute
There is no animation_name prop on ModelViewer, and that is deliberate: 1.0.0 ships no imperative surface — no play(), no pause(), no activateAR(). So this entire page runs through the attributes escape hatch, which makes it the most honest demonstration on the site of what that hatch is for.
| Attribute | Effect |
|---|---|
autoplay | Start playing on load. Without it the model loads in its bind pose, which reads as broken rather than as idle. |
animation-name | Which clip. Changing it switches clip. |
animation-crossfade-duration | Milliseconds to blend between clips. |
paused | Presence pauses. |
dmv.ModelViewer(
id="viewer",
src=ROBOT,
alt="A robot, mid-wave",
attributes={"autoplay": "", "animation-name": "Wave"},
)
Resuming means leaving the key out of the dict, not setting it to False or "false". An HTML boolean attribute is true by presence, so paused="false" is still paused.
The shim removes any attribute that disappears between renders, so building a fresh dict each time — as the callback above does — is the pattern that works. False and None also remove, if you prefer to be explicit.
Crossfade is the setting worth playing with
Set it to 0 and switching clips is a hard cut: the robot teleports from one pose into the next. At 300 ms the poses blend and it reads as a character changing its mind.
The default in <model-viewer> is 300, and it is one of the few upstream defaults this page leaves alone — it is already the right answer, which is worth knowing before you reach for it.
What is not here
Honest about the edges, because the gap is in the package rather than in this page:
- No scrubbing and no speed control.
currentTimeandtimeScaleare
JavaScript properties with no attribute equivalent, so they are unreachable from Python in 1.0.0. A clientside callback can drive them — see the one on Texture Upload for the shape — but there is no prop.
- No "animation finished" event.
<model-viewer>fires no such event, so
there is nothing for the shim to forward.
- No
appendAnimation. Composing clips from several files is an imperative
API.
If you need any of those today, the escape hatch is JavaScript. If enough people need them, they are props — which is the better answer and is not in this release.
Models that actually carry clips
Of the demo models this site uses, only Robot Expressive has animations — measured by reading each .glb's JSON chunk, not assumed. The astronaut, the shoe, the sofa, the chair, the skull and the glass all have none, which is why this page does not offer them: a dropdown that is empty for six of seven choices is how Model Switching used to read before its models were chosen for the feature it documents.
Source: /animation
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:
- /animation/llms.txt — LLM-friendly documentation
- /sitemap.xml
- /robots.txt