Command Palette

Search for a command to run...

Hectal
Shots in this scene
SHOT 16.4Advanced ~21 min · shot 4 of 6

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.

0/6 · 0%

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.

Draw the machine firstdiagram
Rendering diagram…

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. 1

    Let a second click restart the greeting

    In the table example change greeting: { done: "idle" } to greeting: { done: "idle", click: "greeting" }. The line greeting + click: ignored becomes greeting + click -> greeting: one rule decides what a repeated click means.

  2. 2

    Press hover twice, then click, then hover

    In the player example press the buttons in that order. The console prints state: blinking once, then state: greeting once. The repeated hover and the hover during the greeting print nothing. About 1.5 seconds after the greeting started (the greet stretch is 45 frames at 30 fps) it prints state: idle: in our run it came 1,529 ms later. The greeting finished and sent done.

Code & diagrams

A state machine with no animation

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.

Press Run to see the page.
00:00.000
animations
0.00s0.10s0.20s0.30s0.40s0.50s0.60s0.70s0.80s0.90s

Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.

frames

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: ignored
The same machine driving the headlight

The 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`.

Press Run to see the page.
00:00.000
animations
0.00s0.10s0.20s0.30s0.40s0.50s0.60s0.70s0.80s0.90s

Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.

frames

Console

Nothing logged yet.

Expected console output

state: idle
Rive's web API, from its documentation (not run here: Rive is not in this runtime)js
// 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.

terminal
$ Press click, wait 3 seconds
── what you'll see ──
The label stays on `greeting` forever, the mascot freezes on its last frame, and every hover is ignored (`greeting` has no hover rule).

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.json and re-read the docs before an upgrade.

Remember this

  1. 1

    The problem this solves: interactive animation has many moments (arrive, hover, click, wait) that can overlap. Written as scattered if statements 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. 2

    What Rive is. Rive is a design tool and runtime for interactive vector animation. A .riv file 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. 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 (or undefined before); a boolean or number input has .value, a trigger has .fire(); cleanup() frees the instance. We re-read the constructor options (src, canvas, autoplay, stateMachine singular, onLoad), resizeDrawingSurfaceToCanvas() and cleanup() on Rive's web page in this review; that page does not cover stateMachineInputs, .value and .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. 4

    Rive is not in this course's browser runtime, and we could not make a .riv file 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. 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 TABLE of state to event to next state, and a STATES map saying which marker and loop flag each state uses. enter() plays the marker; the player's complete event is sent as the event done.

  6. 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. 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

01

Why is it safer to ignore an event a state does not list than to handle every event everywhere?

02

What goes wrong with boolean flags like isBlinking and isWaving?

03

What does a Rive .riv file contain that a Lottie file does not?

Practice

01

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.

02

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