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/dompnpm add @motionactor/core @motionactor/domyarn add @motionactor/core @motionactor/dombun add @motionactor/core @motionactor/domnub add @motionactor/core @motionactor/domaube 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; // 1Each 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.
What to read next
How it works
Choosing a primitive
Complete Remotion example
Creating actors