Skip to content

Animate with Motion

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"
}
]
}
]
}
}

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.

A progress ring that fills from 0 to 75% over one second, 80×80.
{
"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.

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.

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}).

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.

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.

30
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"
}
}
}
}

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.