State machine inputs & events
How to drive a Lupian animation from your site (inputs) and respond to it (events).
Use this when
- The animation has interactive states (hover, click, progress, scroll, etc.).
- The animation needs to tell your code something happened ("submit button clicked", "sequence finished").
handoff.mdlists inputs or events.
What you need
- An initialised
Riveinstance (from any recipe). - The state machine name from
handoff.md. - The input/event names from
handoff.md(case-sensitive).
Concepts
| Term | Direction | Example |
|---|---|---|
| Input | Your code → animation | isHovering = true to start a hover animation |
| Event | Animation → your code | "buttonClicked" fires when the user clicks the visual button |
Inputs come in three types: boolean (value: true/false), number (value: 0–100, etc.), trigger (call .fire() once).
Reading inputs in Vanilla Web
js
const r = new Rive({
src: "/animations/hero.riv",
canvas,
autoplay: true,
stateMachines: "State Machine 1",
onLoad: () => {
r.resizeDrawingSurfaceToCanvas();
const inputs = r.stateMachineInputs("State Machine 1");
const isHover = inputs.find(i => i.name === "isHovering"); // bool
const progress = inputs.find(i => i.name === "progress"); // number
const onClick = inputs.find(i => i.name === "onClick"); // trigger
canvas.addEventListener("mouseenter", () => { isHover.value = true; });
canvas.addEventListener("mouseleave", () => { isHover.value = false; });
canvas.addEventListener("click", () => { onClick.fire(); });
// Drive a number from scroll, for example
window.addEventListener("scroll", () => {
progress.value = Math.min(100, window.scrollY / 5);
});
},
});Read inputs after onLoad
stateMachineInputs(...) returns an empty array until the file has loaded. Always read inside onLoad or after a "load" event.
Reading inputs in React
tsx
import {
useRive,
useStateMachineInput,
} from "@rive-app/react-webgl2";
export function Button() {
const { rive, RiveComponent } = useRive({
src: "/animations/button.riv",
stateMachines: "Button SM",
autoplay: true,
});
const isHover = useStateMachineInput(rive, "Button SM", "isHovering");
const onClick = useStateMachineInput(rive, "Button SM", "onClick");
return (
<RiveComponent
onMouseEnter={() => isHover && (isHover.value = true)}
onMouseLeave={() => isHover && (isHover.value = false)}
onClick={() => onClick?.fire()}
/>
);
}Listening to events
Events are custom messages the animation fires (set up by Lupian in the Rive editor).
Vanilla Web:
js
new Rive({
src: "/animations/form.riv",
canvas,
autoplay: true,
stateMachines: "State Machine 1",
onRiveEventReceived: (event) => {
if (event.data.name === "submitClicked") {
yourForm.submit();
}
},
});React:
tsx
const { RiveComponent } = useRive({
src: "/animations/form.riv",
stateMachines: "State Machine 1",
autoplay: true,
onRiveEventReceived: (event) => {
if (event.data.name === "submitClicked") {
// ...
}
},
});Event payloads can carry properties (numbers, strings, URLs) — event.data includes them. See rive.app/docs for the full event payload reference.
Common mistakes
- Reading inputs before
onLoad. Returns an empty array. Wrap inonLoad. - Wrong input name. Case-sensitive. Copy from
handoff.mdexactly. - Setting a boolean to a string (
isHover.value = "true"). Use realtrue/false. - Calling
.fire()on a non-trigger input (or.value = ...on a trigger). The types are fixed by Lupian in the editor;handoff.mdlists each one. - Forgetting that React inputs may be
nullon first render. Guard withinput && (...)orinput?.fire(). - Setting a number input outside its declared range. It clamps silently — the animation just looks stuck.
If stuck, send this to Lupian
- The state machine name + input/event names you are using.
- A short clip of the wrong behaviour.
handoff.mdso we can confirm what is wired up.- See Ask Lupian for help.

