---
title: Compositions
description: A composition packages a scene with its size, frame rate, render runtime and props, so any host can build and play it.
---

A composition is the unit a host plays. It names a scene, a size, a frame rate and a render
runtime; its props are the scene's inputs. Remotion, HyperFrames,
the native preview and the exporter all take the same definition, so choosing a host is a
decision made at the app's entry point.

```tsx twoslash title="title-card.tsx"
// @jsx: react-jsx
// ---cut---
import { createScene, input, LayoutActor, signal } from "@motionactor/core";
import { text } from "@motionactor/schema";
import {
  createRenderRuntime,
  defineComposition,
} from "@motionactor/renderers/react";
import { dom } from "@motionactor/dom/react";

class Title extends LayoutActor {
  text = signal("");
}

const titleScene = createScene(
  function* (ctx, { title }) {
    const heading = ctx.spawn(Title, {
      text: title,
      appearance: { opacity: 0 },
    });
    yield* heading.appearance.opacity.tween(1, 0.5);
    yield* ctx.wait(1.5);
    return { heading };
  },
  { inputs: { title: input.of(text, "Launch") } },
);

const runtime = createRenderRuntime({
  backends: [dom],
  renderers: [
    dom.render(Title, ({ actor }) => (
      <h1>{actor.signals.text}</h1>
    )),
  ],
});

export const titleCard = defineComposition({
  id: "TitleCard",
  width: 1920,
  height: 1080,
  fps: 30,
  runtime,
  scene: titleScene,
  background: "#0b0b0f",
});
```

The host builds `scene` at the composition's size and frame rate, with the props as its inputs,
and draws each frame with the binding's `SceneRenderer` over `background`.

## The definition

| Field | Purpose |
| --- | --- |
| `id` | The name hosts list it under. Required |
| `width`, `height`, `fps` | The stage in pixels and the frame rate. Each must be a positive number |
| `scene` | A `createScene` definition, built at the composition's size and frame rate |
| `defaultProps` | Props used when a host passes none, merged under a host's. Required when the scene has a required input |
| `inputs` | `(props: P) → inputs`, when the props are not the scene's inputs. `P` comes from `defaultProps` or the mapper's parameter; the result is checked against the scene's inputs. Without it the props are exactly the scene's inputs |
| `scenes` | `(props) → StagedScene[]`, the scenes the built scene lists as `ctx.scenes` |
| `runtime` | The render runtime: backends and renderers |
| `fonts` | Font faces to load before the first frame is shown. See [Media, assets and fonts](/docs/rendering/media-assets-and-fonts) |
| `background` | The fill under the scene when `render` is left out |
| `render` | `({ width, height, fps, props, compiled, frame }) → element`, called for every frame. Defaults to `SceneRenderer` over `background` |

Props that pick or shape a scene go through `scenes`: a root that runs
`for (const s of ctx.scenes) yield* ctx.stage(s)` plays what `scenes(props)` returns. A build
that compiles to less than one whole frame throws. Hosts read the composition's length from `compiled.totalDuration`,
so nothing else declares a duration.

`render` is where a composition can draw around the scene: a watermark, a debug overlay
reading `props`. `SceneRenderer` inside it needs no `runtime` prop: the composition provides
its runtime to everything it draws. Anything that moves belongs in the scene, where seeking
and export can reach it.

## Building outside a host

`buildComposition` runs the same build a host does and returns the compiled scene, the media
plan and every asset the composition loads. A test or a command-line tool can call it with no
browser:

```ts twoslash
// @jsx: react-jsx
// @filename: title-card.tsx
import { createScene, input, LayoutActor, signal } from "@motionactor/core";
import { text } from "@motionactor/schema";
import {
  createRenderRuntime,
  defineComposition,
} from "@motionactor/renderers/react";
import { dom } from "@motionactor/dom/react";
class Title extends LayoutActor { text = signal(""); }
const titleScene = createScene(function* (ctx, { title }) {
  const heading = ctx.spawn(Title, { text: title, appearance: { opacity: 0 } });
  yield* heading.appearance.opacity.tween(1, 0.5);
  yield* ctx.wait(1.5);
  return { heading };
}, { inputs: { title: input.of(text, "Launch") } });
export const titleCard = defineComposition({
  id: "TitleCard", width: 1920, height: 1080, fps: 30, defaultProps: { title: "Launch" },
  runtime: createRenderRuntime({ backends: [dom] }),
  scene: titleScene,
});
// @filename: main.ts
// ---cut---
import { buildComposition } from "@motionactor/renderers";
import { titleCard } from "./title-card";

using build = buildComposition(titleCard, { title: "Hello" });

build.compiled.totalDuration; // 60
build.compiled.at(0).scene.heading?.text; // "Hello"
build.assets; // []
```

A build owns the cells its scene compiled. `using` releases them when the build leaves scope;
outside a block, call `build[Symbol.dispose]()`. Hosts do the same when props change or the
composition unmounts.

## What a host does with it

1. Merges its props over `defaultProps` and builds the scene, after React has committed, so
   a render React throws away never compiles a scene.
2. Collects the build's assets: the scene's asset fields, every video and audio clip its media
   renderers report, and the composition's fonts.
3. Holds the first frame until those assets, the fonts and every backend are ready.
4. Calls `render` with each frame it is asked for.

When a scene needs the length of a video or audio file to compile, the first build stops with
the source it is missing. The host measures that file in the browser and builds again with the
length known.

## In Remotion

`toRemotionComposition` turns a definition into the props of a Remotion `<Composition>`. The
duration comes from a build in Remotion's `calculateMetadata`, so it follows the props.

```tsx twoslash title="root.tsx"
// @jsx: react-jsx
// @filename: title-card.tsx
import { createScene, input, LayoutActor, signal } from "@motionactor/core";
import { text } from "@motionactor/schema";
import {
  createRenderRuntime,
  defineComposition,
} from "@motionactor/renderers/react";
import { dom } from "@motionactor/dom/react";
class Title extends LayoutActor { text = signal(""); }
const titleScene = createScene(function* (ctx, { title }) {
  const heading = ctx.spawn(Title, { text: title, appearance: { opacity: 0 } });
  yield* heading.appearance.opacity.tween(1, 0.5);
  yield* ctx.wait(1.5);
  return { heading };
}, { inputs: { title: input.of(text, "Launch") } });
export const titleCard = defineComposition({
  id: "TitleCard", width: 1920, height: 1080, fps: 30, defaultProps: { title: "Launch" },
  runtime: createRenderRuntime({ backends: [dom] }),
  scene: titleScene,
});
// @filename: root.tsx
// ---cut---
import { Composition } from "remotion";
import { toRemotionComposition } from "@motionactor/remotion";
import { titleCard } from "./title-card";

export const RemotionRoot = () => (
  <Composition {...toRemotionComposition(titleCard)} />
);
```

The other hosts are on [Hosts](/docs/rendering/hosts).
