Animate with Motion
A first animation
Section titled “A first animation”A Motion node’s props are the animation itself: size, duration (ms), fps and layers, in the dialect, readable JSON that compiles to Lottie. One engine (eui-motion, Rust) samples every frame into a flat display list and each renderer paints that list, so React, Solid, the web component, SwiftUI, Compose and gpui show the same picture. The web renderers load the engine as WebAssembly on the first Motion they paint (about 200 KB gzipped; bundlers emit it as a hashed asset).
{ "component": "Motion", "props": { "size": [ 48, 48 ], "duration": 1000, "label": "Loading", "layers": [ { "type": "shape", "transform": { "position": [ 24, 24 ], "rotation": { "keys": [ { "t": 0, "v": 0 }, { "t": 1000, "v": 360 } ] } }, "shapes": [ { "type": "ellipse", "size": [ 36, 36 ] }, { "type": "trim", "end": 70 }, { "type": "stroke", "color": "$color.primary", "width": 4, "cap": "round" } ] } ] }}Written by a model
Section titled “Written by a model”Each of these was written by a model from the dialect guide alone, on its first try, from one sentence. The prompts and every reply are in the eval.
{ "size": [ 80, 80 ], "duration": 1000, "fps": 60, "layers": [ { "type": "shape", "id": "progress", "shapes": [ { "type": "path", "d": "M40 10 C56.57 10 70 23.43 70 40 C70 56.57 56.57 70 40 70 C23.43 70 10 56.57 10 40 C10 23.43 23.43 10 40 10 Z" }, { "type": "trim", "start": 0, "end": { "keys": [ { "t": 0, "v": 0, "ease": "ease-in-out" }, { "t": 1000, "v": 75 } ] }, "offset": 0 }, { "type": "stroke", "color": "$color.primary", "width": 8, "cap": "round", "join": "round" } ] }, { "type": "shape", "id": "track", "shapes": [ { "type": "ellipse", "position": [ 40, 40 ], "size": [ 60, 60 ] }, { "type": "stroke", "color": "$color.muted", "width": 8 } ] } ]}The guide is part of catalogPrompt(): the Motion entry carries it (about 600 tokens), so an agent that builds surfaces can animate them with no extra setup. What it sends is checked before anyone sees it:
| Where | How |
|---|---|
| Your server | motionPropsProblems(props) from @exponential_app/eui/motion/server: every problem with its code and JSON pointer, to send back to the model. |
| Exponential | exponential_show refuses a card whose Motion does not compile, carries an expression or breaks a limit, naming the path. |
| Your terminal | exponential motion check | compile | decompile | render --gif <file>. |
What the eval taught: ask for one idea per animation, keep it under two seconds, prefer $color tokens so it follows the theme, and give a label unless it is decoration. Limits: 2 MB, 5,000 shapes, 100,000 vertices per frame, 120 s.
Sources
Section titled “Sources”A Motion takes exactly one source.
| Prop | What |
|---|---|
layers (+ size, duration, fps, assets, markers) |
The dialect, inline in the surface. |
lottie |
A Lottie JSON animation as is. Expressions are refused; 3D layers are skipped. |
src |
An attached .json or .lottie file (the first animation of a .lottie, its images inlined), fetched under the host’s media policy. |
Theming
Section titled “Theming”A colour written as $color.primary (or any colour token) becomes a Lottie slot the renderer fills with the theme’s colour for the current mode, and refills when the theme or mode changes; a {light, dark} pair picks its half by mode. A Lottie file is recoloured through slots ({slotId: value}).
Playback
Section titled “Playback”One clock, the engine’s, on every platform.
| Prop or rule | Effect |
|---|---|
autoplay, loop, plays, direction, speed |
Plays when visible; repeats, plays n times, reverses or alternates; scales time. |
segment, marker |
Plays only [from, to] ms, or a named marker’s segment. |
progress (bindable) |
Scrubs to 0–1 instead of playing: bind it to drive the animation from data. |
play (bindable) |
false freezes the clock where it is. |
poster |
The still frame (ms) shown before playing, off-screen and under reduced motion. |
| Frame rate | At most 60 fps (the display’s rate when lower). |
| Visibility | Paused off-screen and while the window is hidden; at most 8 Motions animate per surface (tree order), the rest show their poster. |
| Reduced motion | Shows the poster and stays still. |
Unstyled, the box is the composition’s px size, shrinking to the available width with its aspect kept; a style width or height wins. fit is contain (default), cover or fill.
Interactivity
Section titled “Interactivity”A stateMachine (dotLottie 2 JSON; a .lottie src brings its own first machine) turns a Motion into a control. Its states pick what plays (a segment or a marker, loop, speed, autoplay, final); transitions guard on inputs; interactions (Click, PointerDown/Up, PointerEnter/Exit, OnComplete) run actions on them. The engine interprets the machine, the same code on every platform; a renderer reports what happened and restarts the clock when the state changes. Click the heart; drag the slider past 50 and to 100.
| Prop or event | Effect |
|---|---|
stateMachine |
The machine; while it runs, its current state’s playback replaces the playback props. |
inputs (bindable) |
{name: value} written into the machine on every change: bind it so a data model drives the states. |
| Interactions | A press (pointer, or Enter / Space when focusable) is Click; pointer down / up; hover enters and leaves; a finished play is OnComplete. |
on.press |
A press. With it the Motion is a button: focusable, named by its label. |
on.hover |
The pointer entered. |
on.visible |
The first time half of it is on screen. |
on.complete |
A finite play finished, or the machine reached a final state. |
on.state |
Each state the machine enters, with context {state}. |
{ "component": "Motion", "props": { "stateMachine": { "initial": "off", "inputs": [ { "type": "Boolean", "name": "liked", "value": false } ], "interactions": [ { "type": "Click", "actions": [ { "type": "Toggle", "inputName": "liked" } ] } ], "states": [ { "name": "off", "type": "PlaybackState", "marker": "off", "loop": false, "transitions": [ { "type": "Transition", "toState": "on", "guards": [ { "type": "Boolean", "inputName": "liked", "conditionType": "Equal", "compareTo": true } ] } ] }, { "name": "on", "type": "PlaybackState", "marker": "on", "loop": false, "transitions": [ { "type": "Transition", "toState": "off", "guards": [ { "type": "Boolean", "inputName": "liked", "conditionType": "Equal", "compareTo": false } ] } ] } ] }, "markers": [ { "name": "off", "time": 0, "duration": 300 }, { "name": "on", "time": 300, "duration": 700 } ] }, "on": { "press": { "event": { "name": "like" } } }}Accessibility
Section titled “Accessibility”With a label a Motion is an image named by it; without one it is decorative and hidden from assistive technology. With on.press it is a button named by its label: focusable, pressed with Enter or Space.