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:

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

Keep the token in the environment

.npmrc interpolates ${NODE_AUTH_TOKEN} at install time. Commit the file with the variable reference, never with a token in it.

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

scene.ts
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.

scene.ts
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

frame 0·0.00scard.progress → 0.0000

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.

composition.tsx
import { AbsoluteFill, Composition, registerRoot } from "remotion";
import { defineSceneComposition } from "@motionactor/dom";
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.

On this page