---
title: Remotion card
description: A complete Remotion composition in two files, with a declared child, a derived value read against the viewport and typed scene exports.
---

A card widens and fades in over one second, then holds for one. The example depends on no
actor families and no media.

<Timeline
  fps={30}
  seconds={2}
  tracks={[
    { label: "card.reveal({ duration: 1 })", start: 0, duration: 1, note: "30f" },
    { label: "ctx.wait(1)", start: 1, duration: 1, note: "30f" },
  ]}
>
  60 frames. `progress` reaches 1 at frame 30 and holds; `revealWidth` follows it, scaled to
  three quarters of the stage.
</Timeline>

## scene.ts

```ts twoslash title="scene.ts"
// @filename: scene.ts
// ---cut---
import {
  Actor,
  LayoutActor,
  child,
  createScene,
  derived,
  runtime,
  signal,
  type Duration,
} from "@motionactor/core";

export class Caption extends Actor {
  text = signal("MotionActor");
}

export class Card extends LayoutActor {
  caption = child(Caption);
  progress = signal(0);
  // Three quarters of the stage once revealed.
  revealWidth = derived(() => this.progress() * 0.75 * runtime.viewport().width);

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

export const scene = createScene(function* (ctx) {
  const card = ctx.spawn(Card);
  yield* card.reveal({ duration: 1 });
  yield* ctx.wait(1);
  return { card, caption: card.caption };
});
```

- `progress` is the authored state, and `reveal({ duration })` is the command that moves it.
  One second is 30 frames because the composition builds the scene at 30 fps.
- `caption = child(Caption)` declares the card's caption and creates it with the card. See
  [Children](/docs/runtime/children).
- `revealWidth` is a [`derived`](/docs/reactivity/derived) of `progress` and the stage width,
  which it reads with `runtime.viewport()`. The composition builds the scene at its own
  size, so a build at another size gives another width over the same timeline.
- The scene returns the card and its caption, so the view reads them as typed
  `scene.card` and `scene.caption`.

## composition.tsx

```tsx twoslash title="composition.tsx"
// @jsx: react-jsx
// @filename: scene.ts
import {
  Actor,
  LayoutActor,
  child,
  createScene,
  derived,
  runtime,
  signal,
  type Duration,
} from "@motionactor/core";

export class Caption extends Actor {
  text = signal("MotionActor");
}

export class Card extends LayoutActor {
  caption = child(Caption);
  progress = signal(0);
  // Three quarters of the stage once revealed.
  revealWidth = derived(() => this.progress() * 0.75 * runtime.viewport().width);

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

export const scene = createScene(function* (ctx) {
  const card = ctx.spawn(Card);
  yield* card.reveal({ duration: 1 });
  yield* ctx.wait(1);
  return { card, caption: card.caption };
});
// @filename: composition.tsx
// ---cut---
import { AbsoluteFill, Composition, registerRoot } from "remotion";
import { dom } from "@motionactor/dom/react";
import { createRenderRuntime, defineComposition } from "@motionactor/renderers/react";
import { toRemotionComposition } from "@motionactor/remotion";
import { scene } from "./scene";

export const card = defineComposition({
  id: "ActorCard",
  width: 1280,
  height: 720,
  fps: 30,
  runtime: createRenderRuntime({ backends: [dom] }),
  scene,
  render: ({ compiled, frame }) => {
    const { card, caption } = compiled.at(frame).scene;
    if (!card || !caption) return <AbsoluteFill />;
    // This small composition renders its typed scene exports directly.
    // Reusable actor renderers can instead be dispatched by SceneRenderer.
    return (
      <AbsoluteFill className="items-center justify-center bg-slate-950 text-white">
        <div
          className="overflow-hidden rounded-xl bg-indigo-600 p-8 text-6xl whitespace-nowrap"
          style={{ width: card.revealWidth, opacity: card.progress }}
        >
          {caption.text}
        </div>
      </AbsoluteFill>
    );
  },
});

export function RemotionRoot() {
  return <Composition {...toRemotionComposition(card)} />;
}

registerRoot(RemotionRoot);
```

`defineComposition` declares the composition with a render runtime that has the DOM
backend. `toRemotionComposition` turns it into the props of a Remotion `<Composition>`, with
the duration measured by building the scene. [Compositions](/docs/rendering/compositions) covers
both.

The view reads the two exports and draws them itself. A composition with reusable actors
draws them with `SceneRenderer` and registered [renderers](/docs/rendering/renderers) instead.

## What the check asserts

```ts twoslash
// @filename: scene.ts
import {
  Actor,
  LayoutActor,
  child,
  createScene,
  derived,
  runtime,
  signal,
  type Duration,
} from "@motionactor/core";

export class Caption extends Actor {
  text = signal("MotionActor");
}

export class Card extends LayoutActor {
  caption = child(Caption);
  progress = signal(0);
  // Three quarters of the stage once revealed.
  revealWidth = derived(() => this.progress() * 0.75 * runtime.viewport().width);

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

export const scene = createScene(function* (ctx) {
  const card = ctx.spawn(Card);
  yield* card.reveal({ duration: 1 });
  yield* ctx.wait(1);
  return { card, caption: card.caption };
});
// @filename: check.ts
// ---cut---
import { scene } from "./scene";

using wide = scene.build({ viewport: { width: 800, height: 450 }, fps: 30 });
wide.at(15).scene.card?.progress; // 0.5
wide.at(15).scene.card?.revealWidth; // 300

using narrow = scene.build({ viewport: { width: 400, height: 225 }, fps: 30 });
narrow.at(15).scene.card?.revealWidth; // 150
```

A new stage size is a new build: the timeline is the same, and `revealWidth` takes its share
of the stage the build was given.

## In a Remotion app

Copy both files into a Remotion app and use `composition.tsx` as its entry point. The app
needs React, Remotion, `@motionactor/core`, `@motionactor/renderers`, `@motionactor/dom` and
`@motionactor/remotion`, and Tailwind for the class names in the view. The only inline styles
are the resolved animation values.

If the app already calls `registerRoot`, drop the example's call and add
`<Composition {...toRemotionComposition(card)} />` to the existing root.
