Skip to content
MotionActor
Esc
↑↓navigate↵open⌘Jpreview
On this page

Getting started

Install the packages, compile a scene, resolve a frame, and render it with Remotion.

Install

The @motionactor/* line is published to GitHub Packages, not public npm. Point the scope at the registry in your project’s .npmrc:

@motionactor:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}

GitHub Packages requires authentication for reads as well as writes, so NODE_AUTH_TOKEN must hold a token with the read:packages scope. In CI, secrets.GITHUB_TOKEN is enough.

npm install @motionactor/core @motionactor/dom
pnpm add @motionactor/core @motionactor/dom
yarn add @motionactor/core @motionactor/dom
bun add @motionactor/core @motionactor/dom
nub add @motionactor/core @motionactor/dom
aube add @motionactor/core @motionactor/dom

@motionactor/core is the runtime — actors, signals, scene compilation, frame resolution. @motionactor/dom is the DOM/React backend and the Remotion seam. See Packages for the rest of the line.

Your first scene

Declare an actor

An actor owns state and the commands that move it. LayoutActor is the base for anything that occupies space — it already declares layout, transform, appearance, flexItem and gridItem.

import { LayoutActor, signal, type Duration } from "@motionactor/core";

export class Card extends LayoutActor {
  progress = signal(0);

  *reveal({ duration }: { duration: Duration }) {
    yield* this.progress.tween(1, duration);
  }
}

reveal is a generator. yield* on a tween is what consumes authored time — the method reads like an instruction and compiles into a timeline record.

Compile a scene

createScene runs the generator once and records everything it authored. It does not render, and it does not play.

import { createScene } from "@motionactor/core";

export const FPS = 30;

export const compiled = createScene(
  function* (ctx) {
    const card = ctx.spawn(Card);

    yield* card.reveal({ duration: 1 });
    yield* ctx.wait(1);

    return { card };
  },
  { fps: FPS },
);

Returning { card } makes it a scene export — typed access to that actor from every resolved frame, with no lookup by string and no casts.

Resolve a frame

The scene above is the code on this page, compiled and running. Drag the scrubber: every position calls compiled.at(frame) fresh, so scrubbing backwards is the same operation as playing forwards.

compiled.fps; // 30
compiled.totalDuration; // 60 frames — 1s reveal + 1s hold

compiled.at(0).scene.card.progress; // 0
compiled.at(15).scene.card.progress; // 0.5
compiled.at(30).scene.card.progress; // 1

Each call is independent. Asking for frame 30 does not require having asked for frame 29, and asking twice returns the same answer.

Render it

defineSceneComposition wraps a compiled scene as a Remotion composition: it supplies the render runtime, binds the Remotion host, and derives duration from the scene through calculateMetadata.

import { AbsoluteFill, Composition, registerRoot } from "remotion";
import { defineSceneComposition } from "@motionactor/remotion";
import { compiled, FPS } from "./scene";

export const CardComposition = defineSceneComposition({
  id: "Card",
  width: 1280,
  height: 720,
  fps: FPS,
  buildScene: () => compiled,
  render: ({ compiled, frame }) => {
    const { card } = compiled.at(frame).scene;

    return (
      <AbsoluteFill className="items-center justify-center bg-slate-950">
        <div
          className="rounded-xl bg-indigo-600 p-8 text-6xl text-white"
          style={{ opacity: card.progress, transform: `scale(${0.9 + card.progress * 0.1})` }}
        >
          MotionActor
        </div>
      </AbsoluteFill>
    );
  },
});

export function RemotionRoot() {
  return <Composition {...CardComposition.remotion} />;
}

registerRoot(RemotionRoot);

The view is a thin reader. It never decides when anything happens — it draws the state the runtime resolved for this frame.

How it works

Why compile and resolve are separate stages, and what each one may do.

Choosing a primitive

signal vs reactiveSignal vs derived vs channel, with the rule for each.

Complete Remotion example

The full typechecked example: slots, children, channels, scene exports.

Creating actors

Naming, state layout, render paths, and the rules that keep actors reusable.

Was this page helpful?