{"course":{"slug":"motion-and-audio","title":"Motion, Sound and Music","url":"https://addaly.com/learn/motion-and-audio","level":"intermediate","track":"create"},"lesson":{"slug":"interactive-motion","title":"Motion that responds to state","url":"https://addaly.com/learn/motion-and-audio/interactive-motion","position":25,"of":72,"minutes":10,"answer":"Interactive motion is a state machine, and the hard part is interruption: a tool like Rive blends from the current position automatically, CSS transitions get this free from the browser.","keyIdea":"Interactive motion is a state machine, and the hard part is interruption: a tool like Rive blends from the current position automatically, CSS transitions get this free from the browser, and a play-a-clip approach gets it not at all."},"body":{"format":"markdown","text":"## Animation that has to know what is happening\n\nEverything so far plays. A Lottie plays, a video plays, a sprite sheet plays. Real interface motion frequently has to *respond*: a button that is idle, then hovered, then pressed, then loading, then successful, with a sensible transition between any two of those, including the ones nobody planned for: pressed straight to error, hover interrupted halfway to idle.\n\nBuilding that with play-a-file thinking produces a combinatorial mess. Five states means up to twenty transitions, and a separate clip for each is unmaintainable.\n\n## The state machine\n\nThe tool for this is a finite state machine: a set of named states, a set of inputs, and rules for which input moves you from which state to which other state. The animation for each state, and each transition between states, is authored once.\n\n**Rive** is the product built around this idea. You draw and animate in the editor, then define a state machine in the same file: states, transitions, conditions, and inputs that the host application can set. The runtime loads a `.riv` file and you set inputs from code:\n\n```js\nconst input = stateMachine.findInput('isLoading');\ninput.value = true;   // the runtime handles the blend\n```\n\nThe important part is what the runtime does between states. It blends: if you are 40 per cent through the hover animation when the pointer leaves, it does not snap to idle, it interpolates from where it is. That interruption handling is the thing that is tedious to write by hand and is the main reason to use a tool for it.\n\nRive files are small (commonly under 20KB for a complex interactive component, because it is vector data plus a state graph) and the runtimes cover web, iOS, Android, Flutter and several engines.\n\nBe clear-eyed about the trade: this is a proprietary format with a commercial product behind it. The runtimes are open source and permissively licensed, and the editor has a free tier with limits that change. A file authored in Rive is editable in Rive.\n\n## Doing it without a new dependency\n\nThe same architecture works with tools you already have, and for many components it is the better answer.\n\n**Lottie with named segments.** Author one file with the states laid out along the timeline (frames 0 to 20 idle loop, 21 to 45 hover-in, 46 to 70 press, 71 to 110 success) and drive it from code with `playSegments`. You write the state machine yourself, in about forty lines. You do not get automatic blending on interruption, so design the segments to be interruptible: return to a common pose at the boundaries.\n\n**CSS with a data attribute.** For simple components this is the whole thing. Put the state on the element as `data-state=\"loading\"`, write a rule per state, and let transitions handle the in-between. The browser's transition engine already blends from the current computed value, which means interruption handling is free: this is the one place the platform gives you for nothing what Rive charges for.\n\n```css\n.btn { transition: transform 180ms var(--ease-move); }\n.btn[data-state=\"pressed\"] { transform: scale(0.96); }\n.btn[data-state=\"loading\"] { transform: scale(1); }\n```\n\n**XState or a hand-rolled reducer** for the logic, with any of the above for the visuals. Keeping the state machine in your application code rather than inside an animation file has a real advantage: it is testable, it is in version control next to the component, and a developer can read it without opening a design tool.\n\n## The question to decide it\n\nDoes the *designer* need to own the state machine, or does the *developer*?\n\nIf the motion is complex, the states are visual, and the person iterating on it is a designer who should not be opening a pull request for every timing tweak, a tool like Rive earns its place. If the states are application states (loading, error, empty, authorised) they belong in code, because that is where the truth about them already lives, and duplicating them into an animation file guarantees the two will disagree.\n\nThe failure mode nobody warns about is the third option: the state machine exists in both places. The animation file knows about `isLoading` and so does the component, and six months later one of them has a state the other does not.\n\n## Scroll as an input\n\nA common case of interactive motion is scroll position driving an animation. It used to require a JavaScript listener recalculating on every scroll event, which is main-thread work at exactly the moment the main thread is busiest.\n\nCSS scroll-driven animations now express this declaratively:\n\n```css\n.progress {\n  animation: grow linear;\n  animation-timeline: scroll(root block);\n}\n```\n\nThe browser runs it on the compositor, so it stays smooth while scripts run. Where support is missing it degrades to the animation not running, so design the default state to be correct.\n\n## The free path\n\n- **CSS transitions plus a state attribute**: free, native, and handles interruption correctly by default.\n- **Lottie** with segment control: free format, free runtimes, free authoring in Glaxnimate.\n- **XState**: MIT-licensed state machines, with a free visualiser that draws the graph.\n- **Rive** runtimes are open source; the editor has a free tier, and it is worth knowing because job adverts have started naming it.\n- **Blender** with drivers for non-runtime interactive prototyping.\n\n## The limitation\n\nA state machine makes the transitions explicit, which is its value and also its cost: you now have to decide what happens for every pair, including the fifteen nobody will ever hit. Teams routinely spend a week on transition polish for states that occur in 0.1 per cent of sessions.\n\nAnd an interactive animation cannot be checked by watching it once. It needs a test that drives it through the awkward sequences (pressed during loading, hover during exit, two inputs changing on the same frame) because those are where the blending produces something nobody designed."},"check":{"question":"A component's hover, press, loading and error visuals are driven by a state machine authored inside an animation file, while the application separately tracks the same four states in its own code. Six months later the two disagree. What was the structural mistake?","options":[{"text":"The state machine belonged in one place; duplicating application states into an animation file guarantees the two definitions will drift.","correct":true},{"text":"The animation file should have used named timeline segments instead of a state machine, because segment playback is the only approach that keeps the source of truth in code.","correct":false,"tempting":"Segments are a workable technique but they do not by themselves decide where the state lives; the duplication is the fault."},{"text":"The states were not covered by tests, and any state machine without a test that drives every transition pair will eventually diverge from its specification.","correct":false,"tempting":"Tests would have caught the symptom, but the design fault existed before any test was written."},{"text":"Loading and error are application states rather than visual states, so they should have been kept out of the animation entirely and represented by swapping between two separate animation files, each with its own internal state machine covering only hover and press.","correct":false,"tempting":"Splitting into two files preserves the duplication it claims to solve and adds a second coordination problem."}]},"prev":{"slug":"sprite-sheets","title":"Frames in a grid, and the memory they cost","url":"https://addaly.com/learn/motion-and-audio/sprite-sheets"},"next":{"slug":"hand-over-a-file","title":"Send the developer a file, not a video","url":"https://addaly.com/learn/motion-and-audio/hand-over-a-file"},"authorship":"Founder and series editor: Dr Mustafa Mun. Written by AI models to a written standard. Exam keys checked blind by a second model: 3,665 of 3,666 agreed. Academic review invited.","review":{"status":"Draft, open for academic review"},"cite":"Addaly (2026) 'Motion that responds to state', in Motion, Sound and Music. Edited by M. Mun. Addaly. Available at: https://addaly.com/learn/motion-and-audio/interactive-motion (Accessed: 13 October 2026).","licence":{"name":"CC BY-NC-SA 4.0","url":"https://creativecommons.org/licenses/by-nc-sa/4.0/"},"reader":{"schema":1,"title":"Motion that responds to state","courseTitle":"Motion, Sound and Music","module":{"index":3,"slug":"shipping-the-motion","title":"Shipping the motion"},"answer":null,"keyIdea":"Interactive motion is a state machine, and the hard part is interruption: a tool like Rive blends from the current position automatically, CSS transitions get this free from the browser, and a play-a-clip approach gets it not at all.","hero":{"kind":"plate","image":null,"alt":null,"caption":null},"blocks":[{"t":"h2","id":"animation-that-has-to-know-what-is-happening","runs":[{"text":"Animation that has to know what is happening"}]},{"t":"p","runs":[{"text":"Everything so far plays. A Lottie plays, a video plays, a sprite sheet plays. Real interface motion frequently has to "},{"text":"respond","i":true},{"text":": a button that is idle, then hovered, then pressed, then loading, then successful, with a sensible transition between any two of those, including the ones nobody planned for: pressed straight to error, hover interrupted halfway to idle."}]},{"t":"p","runs":[{"text":"Building that with play-a-file thinking produces a combinatorial mess. Five states means up to twenty transitions, and a separate clip for each is unmaintainable."}]},{"t":"h2","id":"the-state-machine","runs":[{"text":"The state machine"}]},{"t":"p","runs":[{"text":"The tool for this is a finite state machine: a set of named states, a set of inputs, and rules for which input moves you from which state to which other state. The animation for each state, and each transition between states, is authored once."}]},{"t":"p","runs":[{"text":"Rive","b":true},{"text":" is the product built around this idea. You draw and animate in the editor, then define a state machine in the same file: states, transitions, conditions, and inputs that the host application can set. The runtime loads a "},{"text":".riv","code":true},{"text":" file and you set inputs from code:"}]},{"t":"code","lang":"js","label":"JavaScript","text":"const input = stateMachine.findInput('isLoading');\ninput.value = true;   // the runtime handles the blend"},{"t":"p","runs":[{"text":"The important part is what the runtime does between states. It blends: if you are 40 per cent through the hover animation when the pointer leaves, it does not snap to idle, it interpolates from where it is. That interruption handling is the thing that is tedious to write by hand and is the main reason to use a tool for it."}]},{"t":"p","runs":[{"text":"Rive files are small (commonly under 20KB for a complex interactive component, because it is vector data plus a state graph) and the runtimes cover web, iOS, Android, Flutter and several engines."}]},{"t":"p","runs":[{"text":"Be clear-eyed about the trade: this is a proprietary format with a commercial product behind it. The runtimes are open source and permissively licensed, and the editor has a free tier with limits that change. A file authored in Rive is editable in Rive."}]},{"t":"h2","id":"doing-it-without-a-new-dependency","runs":[{"text":"Doing it without a new dependency"}]},{"t":"p","runs":[{"text":"The same architecture works with tools you already have, and for many components it is the better answer."}]},{"t":"p","runs":[{"text":"Lottie with named segments.","b":true},{"text":" Author one file with the states laid out along the timeline (frames 0 to 20 idle loop, 21 to 45 hover-in, 46 to 70 press, 71 to 110 success) and drive it from code with "},{"text":"playSegments","code":true},{"text":". You write the state machine yourself, in about forty lines. You do not get automatic blending on interruption, so design the segments to be interruptible: return to a common pose at the boundaries."}]},{"t":"p","runs":[{"text":"CSS with a data attribute.","b":true},{"text":" For simple components this is the whole thing. Put the state on the element as "},{"text":"data-state=\"loading\"","code":true},{"text":", write a rule per state, and let transitions handle the in-between. The browser’s transition engine already blends from the current computed value, which means interruption handling is free: this is the one place the platform gives you for nothing what Rive charges for."}]},{"t":"code","lang":"css","label":"CSS","text":".btn { transition: transform 180ms var(--ease-move); }\n.btn[data-state=\"pressed\"] { transform: scale(0.96); }\n.btn[data-state=\"loading\"] { transform: scale(1); }"},{"t":"p","runs":[{"text":"XState or a hand-rolled reducer","b":true},{"text":" for the logic, with any of the above for the visuals. Keeping the state machine in your application code rather than inside an animation file has a real advantage: it is testable, it is in version control next to the component, and a developer can read it without opening a design tool."}]},{"t":"h2","id":"the-question-to-decide-it","runs":[{"text":"The question to decide it"}]},{"t":"p","runs":[{"text":"Does the "},{"text":"designer","i":true},{"text":" need to own the state machine, or does the "},{"text":"developer","i":true},{"text":"?"}]},{"t":"p","runs":[{"text":"If the motion is complex, the states are visual, and the person iterating on it is a designer who should not be opening a pull request for every timing tweak, a tool like Rive earns its place. If the states are application states (loading, error, empty, authorised) they belong in code, because that is where the truth about them already lives, and duplicating them into an animation file guarantees the two will disagree."}]},{"t":"p","runs":[{"text":"The failure mode nobody warns about is the third option: the state machine exists in both places. The animation file knows about "},{"text":"isLoading","code":true},{"text":" and so does the component, and six months later one of them has a state the other does not."}]},{"t":"h2","id":"scroll-as-an-input","runs":[{"text":"Scroll as an input"}]},{"t":"p","runs":[{"text":"A common case of interactive motion is scroll position driving an animation. It used to require a JavaScript listener recalculating on every scroll event, which is main-thread work at exactly the moment the main thread is busiest."}]},{"t":"p","runs":[{"text":"CSS scroll-driven animations now express this declaratively:"}]},{"t":"code","lang":"css","label":"CSS","text":".progress {\n  animation: grow linear;\n  animation-timeline: scroll(root block);\n}"},{"t":"p","runs":[{"text":"The browser runs it on the compositor, so it stays smooth while scripts run. Where support is missing it degrades to the animation not running, so design the default state to be correct."}]},{"t":"h2","id":"the-free-path","runs":[{"text":"The free path"}],"role":"free"},{"t":"ul","items":[[{"text":"CSS transitions plus a state attribute","b":true},{"text":": free, native, and handles interruption correctly by default."}],[{"text":"Lottie","b":true},{"text":" with segment control: free format, free runtimes, free authoring in Glaxnimate."}],[{"text":"XState","b":true},{"text":": MIT-licensed state machines, with a free visualiser that draws the graph."}],[{"text":"Rive","b":true},{"text":" runtimes are open source; the editor has a free tier, and it is worth knowing because job adverts have started naming it."}],[{"text":"Blender","b":true},{"text":" with drivers for non-runtime interactive prototyping."}]]},{"t":"h2","id":"the-limitation","runs":[{"text":"The limitation"}],"role":"limit"},{"t":"p","runs":[{"text":"A state machine makes the transitions explicit, which is its value and also its cost: you now have to decide what happens for every pair, including the fifteen nobody will ever hit. Teams routinely spend a week on transition polish for states that occur in 0.1 per cent of sessions."}]},{"t":"p","runs":[{"text":"And an interactive animation cannot be checked by watching it once. It needs a test that drives it through the awkward sequences (pressed during loading, hover during exit, two inputs changing on the same frame) because those are where the blending produces something nobody designed."}]}],"check":{"question":[{"text":"A component’s hover, press, loading and error visuals are driven by a state machine authored inside an animation file, while the application separately tracks the same four states in its own code. Six months later the two disagree. What was the structural mistake?"}],"options":[{"text":[{"text":"The state machine belonged in one place; duplicating application states into an animation file guarantees the two definitions will drift."}],"correct":true,"tempting":null},{"text":[{"text":"The animation file should have used named timeline segments instead of a state machine, because segment playback is the only approach that keeps the source of truth in code."}],"correct":false,"tempting":[{"text":"Segments are a workable technique but they do not by themselves decide where the state lives; the duplication is the fault."}]},{"text":[{"text":"The states were not covered by tests, and any state machine without a test that drives every transition pair will eventually diverge from its specification."}],"correct":false,"tempting":[{"text":"Tests would have caught the symptom, but the design fault existed before any test was written."}]},{"text":[{"text":"Loading and error are application states rather than visual states, so they should have been kept out of the animation entirely and represented by swapping between two separate animation files, each with its own internal state machine covering only hover and press."}],"correct":false,"tempting":[{"text":"Splitting into two files preserves the duplication it claims to solve and adds a second coordination problem."}]}],"order":[3,0,2,1],"why":null},"prev":{"slug":"sprite-sheets","title":"Frames in a grid, and the memory they cost","minutes":9},"next":{"slug":"hand-over-a-file","title":"Send the developer a file, not a video","minutes":9}}}