State Machines

Load and run dotLottie state machines from C, forward pointer input, drive transitions with typed inputs, and poll transition events.

A state machine turns an animation into something interactive. Instead of playing start to finish, the animation sits in a named state and moves between states in response to input — a pointer entering the canvas, a click, a numeric value crossing a threshold.

The machine is authored alongside the animation and packaged in the .lottie file, so behavior lives with the artwork rather than in your code. Your job in C is to load it, forward input, and tick it.

For the concepts behind state machines, see the state machine explanation — the model is identical across every dotLottie player.

State machines require the state-machines feature, which is enabled by default.

Load and start

A state machine is a second handle with its own lifetime, borrowed from the player.

dotlottieDotLottieStateMachine *machine =
  dotlottie_state_machine_load(player, "button-states");

if (!machine) {
  /* no machine with that ID, or the state-machines feature is off */
}

Load a definition from your own JSON instead with dotlottie_state_machine_load_data().

Starting it takes a security policy for any OpenUrl actions the machine might perform:

dotlottie_state_machine_start(machine, "https://lottiefiles.com", true);
ParameterMeaning
whitelistComma-separated list of permitted URL prefixes. NULL or "" permits none
require_user_interactionWhen true, a URL only opens as a result of genuine user input
Treat the URL whitelist as a security boundary

A .lottie file is data, and a state machine inside one can ask to open a URL. If you load files you did not author, pass a restrictive whitelist and keep require_user_interaction set to true. Passing NULL denies all URL opening, which is the safest default.

Starting resets playback: the machine stops the player, rewinds to the first frame, and sets speed to 1.0, forward mode, no looping, and no autoplay. Configure playback after starting, not before.

Tick the machine, not the player

Once a machine is running, drive it with dotlottie_state_machine_tick() instead of dotlottie_tick(). It advances the animation and also evaluates transitions and guards.

bool rendered = false;
dotlottie_state_machine_tick(machine, dt, &rendered);

if (rendered) {
  present(buffer, width, height);
}

As with dotlottie_tick(), dt is the elapsed time in seconds.

Forward input

The runtime has no window system, so it cannot observe input itself. Translate your platform's events into canvas coordinates and post them.

dotlottie_state_machine_post_pointer_move(machine, x, y);
dotlottie_state_machine_post_pointer_down(machine, x, y);
dotlottie_state_machine_post_pointer_up(machine, x, y);
dotlottie_state_machine_post_click(machine, x, y);
dotlottie_state_machine_post_pointer_enter(machine, x, y);
dotlottie_state_machine_post_pointer_exit(machine, x, y);

Coordinates are in pixels relative to the render target's top-left corner. If you scaled or offset the animation with layout or a viewport, convert from window coordinates before posting, or hit tests will be measured against the wrong area.

Only post what the machine uses

dotlottie_state_machine_get_framework_setup() reports which interactions the loaded machine actually listens for, as a bitmask. Use it to skip the work of tracking input nothing responds to — pointer move especially, which fires constantly.

uint16_t interactions = 0;
dotlottie_state_machine_get_framework_setup(machine, &interactions);

bool needs_move = (interactions & (1 << 4)) != 0;
BitValueInteraction
1 << 01Pointer up
1 << 12Pointer down
1 << 24Pointer enter
1 << 38Pointer exit
1 << 416Pointer move
1 << 532Click
1 << 664On complete
1 << 7128On loop complete

A value of 0 means the machine is driven entirely by inputs and events rather than pointer interaction.

These constants aren't in the header

The bitmask type is internal to the runtime and cbindgen does not emit it, so the values above are the reference. Define your own named constants for readability.

Inputs

Inputs are named variables the machine's guards read. Setting one can trigger a transition immediately.

dotlottie_state_machine_set_numeric_input(machine, "progress", 0.75f);
dotlottie_state_machine_set_string_input(machine, "status", "loading");
dotlottie_state_machine_set_boolean_input(machine, "enabled", true);

Read them back with the matching getters:

float progress = 0.0f;
dotlottie_state_machine_get_numeric_input(machine, "progress", &progress);

bool enabled = false;
dotlottie_state_machine_get_boolean_input(machine, "enabled", &enabled);

String inputs use the two-call pattern, since they return text:

size_t size = 0;
if (dotlottie_state_machine_get_string_input(machine, "status", NULL, &size) == DOTLOTTIE_SUCCESS) {
  char *status = malloc(size);
  dotlottie_state_machine_get_string_input(machine, "status", status, NULL);
  /* ... */
  free(status);
}

Custom events

Where inputs represent state, events represent a moment. Fire one by name:

dotlottie_state_machine_fire_event(machine, "submit");

The machine must define a transition listening for that name; unrecognized names are ignored.

Inspect the machine

size_t size = 0;
if (dotlottie_state_machine_get_current_state(machine, NULL, &size) == DOTLOTTIE_SUCCESS) {
  char *state = malloc(size);
  dotlottie_state_machine_get_current_state(machine, state, NULL);
  printf("current state: %s\n", state);
  free(state);
}

dotlottie_state_machine_get_status() reports the engine's status the same way.

For reproducible behavior in machines that make random choices, fix the seed:

dotlottie_state_machine_set_seed(machine, 42);

Observe transitions

State machines have their own event queue, drained like the player's:

dotlottieStateMachineEvent event;

while (dotlottie_state_machine_poll_event(machine, &event) == 1) {
  switch (event.event_type) {
    case StateMachineTransition:
      printf("%s -> %s\n",
             event.data.transition.previous_state,
             event.data.transition.new_state);
      break;
    case StateMachineStateEntered:
      printf("entered %s\n", event.data.state.state);
      break;
    case StateMachineNumericInputChange:
      printf("%s: %.2f -> %.2f\n",
             event.data.numeric_input.name,
             event.data.numeric_input.old_value,
             event.data.numeric_input.new_value);
      break;
    default:
      break;
  }
}
Event typeNumericPayload member
StateMachineStart0
StateMachineStop1
StateMachineTransition2transition
StateMachineStateEntered3state
StateMachineStateExit4state
StateMachineCustomEvent5message
StateMachineError6message
StateMachineStringInputChange7string_input
StateMachineNumericInputChange8numeric_input
StateMachineBooleanInputChange9boolean_input
StateMachineInputFired10input_fired
Copy event strings before polling again

Every string pointer in a state machine event points into library-owned memory that the next poll call reuses. Use strdup() or copy into your own buffer for anything you keep beyond the current iteration.

A second queue, drained with dotlottie_state_machine_poll_internal_event(), carries messages intended for platform integrations. Most applications can ignore it.

Stop and release

dotlottie_state_machine_stop() halts the machine but keeps the handle usable. dotlottie_state_machine_release() destroys it.

dotlottie_state_machine_stop(machine);
dotlottie_state_machine_release(machine);
dotlottie_destroy(player);
Release the machine before destroying the player

The machine holds an internal borrow of the player, so the player must outlive it. Destroying the player first leaves the machine handle dangling.

Putting it together

dotlottieDotLottieStateMachine *machine =
  dotlottie_state_machine_load(player, "button-states");

if (!machine) {
  return 1;
}

dotlottie_state_machine_start(machine, NULL, true);

uint16_t interactions = 0;
dotlottie_state_machine_get_framework_setup(machine, &interactions);
bool needs_move = (interactions & (1 << 4)) != 0;

double last = now_seconds();

for (;;) {
  double current = now_seconds();
  float dt = (float)(current - last);
  last = current;

  input_event in;
  while (next_input_event(&in)) {
    switch (in.kind) {
      case INPUT_MOVE:
        if (needs_move) {
          dotlottie_state_machine_post_pointer_move(machine, in.x, in.y);
        }
        break;
      case INPUT_DOWN:
        dotlottie_state_machine_post_pointer_down(machine, in.x, in.y);
        break;
      case INPUT_UP:
        dotlottie_state_machine_post_pointer_up(machine, in.x, in.y);
        dotlottie_state_machine_post_click(machine, in.x, in.y);
        break;
    }
  }

  bool rendered = false;
  dotlottie_state_machine_tick(machine, dt, &rendered);

  dotlottieStateMachineEvent event;
  while (dotlottie_state_machine_poll_event(machine, &event) == 1) {
    handle_machine_event(&event);
  }

  if (rendered) {
    present(buffer, width, height);
  }
}

dotlottie_state_machine_release(machine);
dotlottie_destroy(player);

Learn more

Last updated: August 13, 2026 at 9:17 AMEdit this page