Shots in this scene
Rive and State Machines
In one line
Rive files carry a state machine: states, inputs and transitions that decide how the animation reacts. The same idea is easy to build in JavaScript, with a table of states driving dotLottie segments. Rive's own API is shown here as code, not run.
Think of it like this
A traffic light controller. It is always in exactly one state (red, green, amber). Events (a timer, a pedestrian's button) move it along fixed rules, and an event that makes no sense in a state, like a button press while already red, is ignored. A character on a page behaves the same way: it should not start a second wave in the middle of the first.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- state machine
- A set of named states, the events that move between them, and the rules for which events each state accepts.
- input
- A named value or button the page sets so a Rive state machine can react: boolean, number or trigger.
- trigger
- An input that you fire once, like pressing a button. It has no lasting value.
- transition
- A move from one state to another, caused by an event.
- .riv file
- Rive's file format: artwork, animations and state machines.
- ignored event
- An event the current state has no rule for. The machine stays where it is.
Step by step
01Draw the machine first
Three states: idle (loops quietly), blinking (plays the blink once), greeting (plays the greeting once). Hover moves idle to blinking, click moves idle or blinking to greeting, and a finished segment (done) always returns to idle.
02Write it as a table, with no player
The first example is only the table and a send() function. It runs a scripted list of events and prints each result, including the ignored ones. You can test a machine like this without any animation.
03Attach the segments
The second example adds the player. enter(name) sets the loop flag and marker for that state and plays. The complete event is sent back into the machine as done.
04Try to confuse it
Press hover twice quickly, then click, then hover. The console shows state: blinking, then state: greeting, and nothing for the repeated and the late hover: the table ignores them.
Try it yourself
- 1
Let a second click restart the greeting
In the table example change
greeting: { done: "idle" }togreeting: { done: "idle", click: "greeting" }. The linegreeting + click: ignoredbecomesgreeting + click -> greeting: one rule decides what a repeated click means. - 2
Press hover twice, then click, then hover
In the player example press the buttons in that order. The console prints
state: blinkingonce, thenstate: greetingonce. The repeated hover and the hover during the greeting print nothing. About 1.5 seconds after the greeting started (thegreetstretch is 45 frames at 30 fps) it printsstate: idle: in our run it came 1,529 ms later. The greeting finished and sentdone.
Code & diagrams
Read each line as `state + event`. Events a state does not list (`hover` while greeting, `click` while greeting, the unknown `wave`) are ignored and the state does not change.
Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.
Console
Nothing logged yet.Expected console output
idle + hover -> blinking
blinking + done -> idle
idle + click -> greeting
greeting + hover: ignored
greeting + click: ignored
greeting + done -> idle
idle + wave: ignoredThe console prints only `state: idle` at load; every other line depends on what you press. Press **click** and the greeting runs once and the label returns to `idle` by itself, because the player's `complete` event is sent as `done`.
Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.
Console
Nothing logged yet.Expected console output
state: idle// package and options as named on rive.app/docs/runtimes/web (read 2026-10-23). headlight.riv is imaginary: we have no Rive file.
import { Rive } from "@rive-app/webgl2";
const canvas = document.getElementById("mascot");
const r = new Rive({
src: "/headlight.riv",
canvas,
autoplay: true,
stateMachine: "Headlight", // the state machine's name, set in the Rive editor
onLoad: () => {
r.resizeDrawingSurfaceToCanvas();
const inputs = r.stateMachineInputs("Headlight"); // from Rive's older docs, not re-confirmed: undefined before the file has loaded
const hover = inputs.find((i) => i.name === "hover"); // a boolean input
const wave = inputs.find((i) => i.name === "wave"); // a trigger input
canvas.addEventListener("pointerenter", () => { hover.value = true; });
canvas.addEventListener("pointerleave", () => { hover.value = false; });
canvas.addEventListener("click", () => wave.fire());
},
});
// when the component is removed: r.cleanup();Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Forget to send `done`
In the player example, delete the line p.addEventListener("complete", () => send("done"));. Press click.
Myth vs fact
Myth
A state machine is only for big apps.
Fact
Three states and six lines of table already replace a pile of boolean flags, and they can be tested without the animation.
Myth
Rive is just a faster Lottie.
Fact
It is a different format with built-in interactive logic. Speed and size claims for either come from the vendors, and we found no independent benchmark.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
A table-driven machine makes tests easy: feed it events and compare the states, with no canvas (Topic 18.4 on testing). The first example is already such a test.
- ▸
Decide what a machine does when the page's motion setting turns off (Topic 7.5). The real mascot (Topic 16.6) answers with a still poster frame and plays nothing.
- ▸
Rive's docs mark direct input setting as being replaced by view models. Pin the runtime version in
package.jsonand re-read the docs before an upgrade.
Remember this
- 1
The problem this solves: interactive animation has many moments (arrive, hover, click, wait) that can overlap. Written as scattered
ifstatements and flags (isWaving,isBlinking) the code breaks as soon as two things happen together. A state machine writes down the allowed states and the events that move between them, in one table. - 2
What Rive is. Rive is a design tool and runtime for interactive vector animation. A
.rivfile contains the artwork, the animations and the state machine. In the editor a designer defines inputs (a boolean, a number, a trigger) and transitions; your page sets the inputs and the file's own logic chooses what to play. The designer owns the behaviour, and you do not rewrite it in code. - 3
Rive's web API (from Rive's documentation, read 2026-10-23, shown below as non-live code):
new Rive({ src, canvas, autoplay, stateMachine: "name", onLoad });stateMachineInputs("name")returns the machine's inputs once it is loaded (orundefinedbefore); a boolean or number input has.value, a trigger has.fire();cleanup()frees the instance. We re-read the constructor options (src,canvas,autoplay,stateMachinesingular,onLoad),resizeDrawingSurfaceToCanvas()andcleanup()on Rive's web page in this review; that page does not coverstateMachineInputs,.valueand.fire(), so those three come from Rive's older docs and we could not re-confirm them. Rive's newer docs also describe data binding with view models (autoBind), which we do not teach here. - 4
Rive is not in this course's browser runtime, and we could not make a
.rivfile here, so no Rive code on this page runs. That is a limit of the course, not a verdict on Rive. Check Rive's current docs before you copy the code. - 5
Build the idea with dotLottie. A dotLottie file may also carry state machines, but ours does not. So the live example keeps the machine in JavaScript: a
TABLEof state to event to next state, and aSTATESmap saying which marker and loop flag each state uses.enter()plays the marker; the player'scompleteevent is sent as the eventdone. - 6
Two rules make a machine reliable. First, an event a state does not list is ignored, which turns rapid triggers and interruptions into a design decision made once in the table: here hovering while blinking does nothing, but clicking while blinking waves. Second, there is one place that enters states, so the visible state, the segment and the log never disagree.
- 7
Lottie versus Rive, honestly. Rive's strength is the built-in state logic and designer ownership of behaviour. Lottie's strength is the format's reach (many tools and players) and a plain JSON you can read, as in Topic 16.2. Vendors publish file-size and speed comparisons; every one we found was written by a company selling one side, and we found no independent benchmark, so we give no numbers. Measure your own files (Topic 16.5).
Explain it without notes
Why is it safer to ignore an event a state does not list than to handle every event everywhere?
What goes wrong with boolean flags like isBlinking and isWaving?
What does a Rive .riv file contain that a Lottie file does not?
Practice
Add a state sleeping to the table: after 10 idle seconds the mascot sleeps; any hover or click wakes it into greeting. Write only the table changes.
Write a test with a few lines of code that proves greeting + hover is ignored.
Trade-offs
- ↔
A JavaScript machine is plain and testable but you maintain it. A Rive machine lets the designer own behaviour but adds a runtime and a file format.
Done when you can
You can draw a state machine for an interactive animation and write it as a table
You can explain why an event can be ignored and why that helps
You can read Rive's basic web API and say what is not verified here
You can say honestly what is and is not known about Lottie versus Rive size