Remotion card
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.
card.reveal({ duration: 1 })ctx.wait(1)60 frames. progress reaches 1 at frame 30 and holds; revealWidth follows it, scaled to
three quarters of the stage.
scene.ts
import {
class Actor<TEvents extends ActorEventMap = {}>Actor,
class LayoutActor<TEvents extends ActorEventMap = {}>LayoutActor,
function child<TBlueprint extends ActorClass<object>, TOwner extends object = object>(blueprint: TBlueprint, ...[overrides]: ChildOverridesArg<TBlueprint, TOwner>): ChildField<TBlueprint, TOwner>child,
function createScene<TSceneExports, const I extends SceneInputs = Readonly<Record<never, SceneInput>>, const L extends SceneLayers = Readonly<Record<never, SceneLayer<BackendRef<string>>>>>(script: SceneScript<I, L, TSceneExports>, options?: SceneDefinitionOptions<I, L>): SceneDefinition<I, L, TSceneExports>Declare a scene. Nothing compiles until a host builds it:
`scene.build({ viewport, fps, inputs })` runs the script as
`(ctx, inputs, viewport, layers)` and returns the compiled scene.
`ctx.spawn` places a root actor in the default layer; `layers.<key>.spawn`
places it in a declared one. `ctx.stage` stages another scene on this
scene's clock, and a staged scene may stage again.createScene,
function derived<TThis, T>(fn: (this: TThis) => T): DerivedDefinition<T, TThis>derived,
const runtime: {
frame(): number;
fps(): number;
timeSeconds(): number;
viewport(): SceneViewport;
}
runtime,
function signal<T>(): RequiredSignal<T> (+1 overload)
module signal
A timeline value on an actor, and an input to every spawn of it.
Without an initial value the signal is required: `ctx.spawn` must supply it.signal,
type type Duration = numberDuration,
} from "@motionactor/core";
export class class CaptionCaption extends class Actor<TEvents extends ActorEventMap = {}>Actor {
Caption.text: ActorFieldSignal<string>text = signal<string>(initial: string): ActorFieldSignal<string> (+1 overload)A timeline value on an actor, and an input to every spawn of it.
Without an initial value the signal is required: `ctx.spawn` must supply it.signal("MotionActor");
}
export class class CardCard extends class LayoutActor<TEvents extends ActorEventMap = {}>LayoutActor {
Card.caption: ChildField<typeof Caption, object>caption = child<typeof Caption, object>(blueprint: typeof Caption, overrides?: ChildOverrides<typeof Caption, object> | undefined): ChildField<typeof Caption, object>child(class CaptionCaption);
Card.progress: ActorFieldSignal<number>progress = signal<number>(initial: number): ActorFieldSignal<number> (+1 overload)A timeline value on an actor, and an input to every spawn of it.
Without an initial value the signal is required: `ctx.spawn` must supply it.signal(0);
// Three quarters of the stage once revealed.
Card.revealWidth: DerivedDefinition<number, unknown>revealWidth = derived<unknown, number>(fn: (this: unknown) => number): DerivedDefinition<number, unknown>derived(() => this.Card.progress: () => numberprogress() * 0.75 * const runtime: {
frame(): number;
fps(): number;
timeSeconds(): number;
viewport(): SceneViewport;
}
runtime.function viewport(): SceneViewportviewport().SceneViewport.width: numberwidth);
*Card.reveal({ duration }: {
duration: Duration;
}): Generator<number, void, unknown>
reveal({ duration: numberduration }: { duration: numberduration: type Duration = numberDuration }) {
yield* this.Card.progress: ActorFieldSignal<number>progress.LiveSignal<number>.tween(target: number, options: number | SignalTweenOptions<number>): Generator<Frame, void, unknown>Tween to target over authored seconds — must yield*tween(1, duration: numberduration);
}
}
export const const scene: SceneDefinition<Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, {
card: Card;
caption: ChildField<typeof Caption, object>;
}>
scene = createScene<{
card: Card;
caption: ChildField<typeof Caption, object>;
}, Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>>(script: SceneScript<Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, {
card: Card;
caption: ChildField<typeof Caption, object>;
}>, options?: SceneDefinitionOptions<...> | undefined): SceneDefinition<...>
Declare a scene. Nothing compiles until a host builds it:
`scene.build({ viewport, fps, inputs })` runs the script as
`(ctx, inputs, viewport, layers)` and returns the compiled scene.
`ctx.spawn` places a root actor in the default layer; `layers.<key>.spawn`
places it in a declared one. `ctx.stage` stages another scene on this
scene's clock, and a staged scene may stage again.createScene(function* (ctx: SceneContextctx) {
const const card: Cardcard = ctx: SceneContextctx.CompileContext.spawn<Card>(definition: ActorClass<Card>, overrides?: {
id?: undefined;
cursor?: undefined;
cursorSeconds?: undefined;
sceneFps?: undefined;
onInit?: undefined;
onEnter?: undefined;
onExit?: undefined;
enter?: undefined;
exit?: undefined;
parent?: undefined;
measuredBox?: undefined;
caption?: {
text?: SignalInputValue<string> | undefined;
} | undefined;
progress?: SignalInputValue<number> | undefined;
revealWidth?: undefined;
reveal?: undefined;
layout?: {
x?: SignalInputValue<LayoutValue> | undefined;
y?: SignalInputValue<...> | undefined;
width?: Readonly<...> | ... 1 more ... | undefined;
height?: Readonly<...> | ... 1 more ... | undefined;
} | undefined;
... 10 more ...;
bounds?: undefined;
} | undefined, options?: {
...;
} | undefined): Card (+1 overload)
spawn(class CardCard);
yield* const card: Cardcard.Card.reveal({ duration }: {
duration: Duration;
}): Generator<number, void, unknown>
reveal({ duration: numberduration: 1 });
yield* ctx: SceneContextctx.CompileContext.wait(duration: Duration): Generator<Frame, void, unknown>wait(1);
return { card: Cardcard, caption: ChildField<typeof Caption, object>caption: const card: Cardcard.Card.caption: ChildField<typeof Caption, object>caption };
});
progressis the authored state, andreveal({ 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.revealWidthis aderivedofprogressand the stage width, which it reads withruntime.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.cardandscene.caption.
composition.tsx
import { const AbsoluteFill: React.ComponentType<Omit<AbsoluteFillElementProps, "durationInFrames" | "freeze" | "from" | "hidden" | "name" | "showInTimeline" | "trimBefore"> & InteractiveBaseProps & {
readonly stack?: string | undefined;
}>
AbsoluteFill, const Composition: <Schema extends AnyZodObject, Props extends Record<string, unknown>>(props: CompositionProps<Schema, Props>) => import("react/jsx-runtime").JSX.Element | nullComposition, const registerRoot: (comp: React.FC<{}>) => voidregisterRoot } from "remotion";
import { const dom: StagedBackend<React.FC<ReactBackendHostProps>, "dom", React.ReactNode>The React DOM backend, and the `dom` binding code names: `dom.render(Box, …)`,
`createRenderRuntime({ backends: [dom] })`.dom } from "@motionactor/dom/react";
import { const createRenderRuntime: (input: CreateRenderRuntimeInput<ReactBackendHost, React.ReactNode>) => ReactRenderRuntimeThe floor's `createRenderRuntime` for React: every backend's host is a
React component and its stages wrap React nodes, so mixed backends (each
with its own host props) compose without naming a type.createRenderRuntime, const defineComposition: DefineComposition<ReactBackendHost, React.ReactNode, React.ReactElement<unknown, string | React.JSXElementConstructor<any>>>A composition of a `createScene` definition (`scene`), painted by `render`,
or by default by `SceneRenderer` over `background`.defineComposition } from "@motionactor/renderers/react";
import { function toRemotionComposition<P extends Record<string, unknown>, T>(input: CompositionDefinition<P, T>): {
id: string;
width: number;
height: number;
fps: number;
defaultProps: P;
durationInFrames: number;
component: React.FC<P>;
calculateMetadata: CalculateMetadataFunction<P>;
}
A Remotion composition for `input`: like every host, it mounts the runtime media family after the runtime's own renderers.
The mounted instance builds at Remotion's resolved viewport, which `calculateMetadata` or a Player may set apart from the definition's.toRemotionComposition } from "@motionactor/remotion";
import { const scene: SceneDefinition<Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, {
card: Card;
caption: ChildField<typeof Caption, object>;
}>
scene } from "./scene";
export const const card: CompositionDefinition<HostInputArgs<Readonly<Record<never, SceneInput>>>, {
card: Card;
caption: ChildField<typeof Caption, object>;
}, ReactBackendHost, React.ReactNode, React.ReactElement<unknown, string | React.JSXElementConstructor<any>>>
card = defineComposition<SceneDefinition<Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, {
card: Card;
caption: ChildField<typeof Caption, object>;
}>>(definition: SceneCompositionInput<SceneDefinition<Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, {
card: Card;
caption: ChildField<typeof Caption, object>;
}>, ReactBackendHost, React.ReactNode, React.ReactElement<...>>): CompositionDefinition<...> (+1 overload)
`scene`'s inputs are the props.defineComposition({
id: stringid: "ActorCard",
width: numberwidth: 1280,
height: numberheight: 720,
fps: numberfps: 30,
runtime: RenderRuntime<ReactBackendHost, React.ReactNode>runtime: function createRenderRuntime(input: CreateRenderRuntimeInput<ReactBackendHost, React.ReactNode>): ReactRenderRuntimeThe floor's `createRenderRuntime` for React: every backend's host is a
React component and its stages wrap React nodes, so mixed backends (each
with its own host props) compose without naming a type.createRenderRuntime({ backends: readonly AnyBackend<ReactBackendHost, React.ReactNode>[]backends: [const dom: StagedBackend<React.FC<ReactBackendHostProps>, "dom", React.ReactNode>The React DOM backend, and the `dom` binding code names: `dom.render(Box, …)`,
`createRenderRuntime({ backends: [dom] })`.dom] }),
scene: SceneDefinition<Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, {
card: Card;
caption: ChildField<typeof Caption, object>;
}>
scene,
render?(args: CompositionRenderArgs<HostInputArgs<Readonly<Record<never, SceneInput>>>, {
card: Card;
caption: ChildField<typeof Caption, object>;
}>): React.ReactElement<unknown, string | React.JSXElementConstructor<any>>
render: ({ compiled: CompiledScene<{
card: Card;
caption: ChildField<typeof Caption, object>;
}, Readonly<Record<string, CompiledLayer<BackendRef<string>>>>>
compiled, frame: numberframe }) => {
const { const card: ResolvedActorOfInstance<Card> | undefinedcard, const caption: ResolvedActorOfInstance<Caption> | undefinedcaption } = compiled: CompiledScene<{
card: Card;
caption: ChildField<typeof Caption, object>;
}, Readonly<Record<string, CompiledLayer<BackendRef<string>>>>>
compiled.CompiledScene<{ card: Card; caption: ChildField<typeof Caption, object>; }, Readonly<Record<string, CompiledLayer<BackendRef<string>>>>>.at(frame: Frame, options?: FrameResolutionOptions): FrameState<{
card: Card;
caption: ChildField<typeof Caption, object>;
}>
at(frame: numberframe).FrameState<{ card: Card; caption: ChildField<typeof Caption, object>; }>.scene: {
card: ResolvedActorOfInstance<Card> | undefined;
caption: ResolvedActorOfInstance<Caption> | undefined;
}
scene;
if (!const card: ResolvedActorOfInstance<Card> | undefinedcard || !const caption: ResolvedActorOfInstance<Caption> | undefinedcaption) return <const AbsoluteFill: React.ComponentType<Omit<AbsoluteFillElementProps, "durationInFrames" | "freeze" | "from" | "hidden" | "name" | "showInTimeline" | "trimBefore"> & InteractiveBaseProps & {
readonly stack?: string | undefined;
}>
AbsoluteFill />;
// This small composition renders its typed scene exports directly.
// Reusable actor renderers can instead be dispatched by SceneRenderer.
return (
<const AbsoluteFill: React.ComponentType<Omit<AbsoluteFillElementProps, "durationInFrames" | "freeze" | "from" | "hidden" | "name" | "showInTimeline" | "trimBefore"> & InteractiveBaseProps & {
readonly stack?: string | undefined;
}>
AbsoluteFill className?: string | undefinedclassName="items-center justify-center bg-slate-950 text-white">
<React.JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>div
React.HTMLAttributes<T>.className?: string | undefinedclassName="overflow-hidden rounded-xl bg-indigo-600 p-8 text-6xl whitespace-nowrap"
React.HTMLAttributes<HTMLDivElement>.style?: React.CSSProperties | undefinedstyle={{ StandardLonghandProperties<string | number, string & {}>.width?: Property.Width<string | number> | undefinedThis feature is well established and works across many devices and browser versions. It’s been available across browsers since July 2015.
**Syntax**: `auto | <length-percentage [0,∞]> | min-content | max-content | fit-content | fit-content(<length-percentage [0,∞]>) | <calc-size()> | <anchor-size()>`
**Initial value**: `auto`
| Chrome | Firefox | Safari | Edge | IE |
| :----: | :-----: | :----: | :----: | :---: |
| **1** | **1** | **1** | **12** | **4** |width: const card: ResolvedActorOfInstance<Card>card.revealWidth: numberrevealWidth, StandardLonghandProperties<string | number, string & {}>.opacity?: Property.Opacity | undefinedThis feature is well established and works across many devices and browser versions. It’s been available across browsers since July 2015.
**Syntax**: `<opacity-value>`
**Initial value**: `1`
| Chrome | Firefox | Safari | Edge | IE |
| :----: | :-----: | :----: | :----: | :---: |
| **1** | **1** | **2** | **12** | **9** |opacity: const card: ResolvedActorOfInstance<Card>card.progress: numberprogress }}
>
{const caption: ResolvedActorOfInstance<Caption>caption.text: stringtext}
</React.JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>div>
</const AbsoluteFill: React.ComponentType<Omit<AbsoluteFillElementProps, "durationInFrames" | "freeze" | "from" | "hidden" | "name" | "showInTimeline" | "trimBefore"> & InteractiveBaseProps & {
readonly stack?: string | undefined;
}>
AbsoluteFill>
);
},
});
export function function RemotionRoot(): JSX.ElementRemotionRoot() {
return <const Composition: <Schema extends AnyZodObject, Props extends Record<string, unknown>>(props: CompositionProps<Schema, Props>) => import("react/jsx-runtime").JSX.Element | nullComposition {...toRemotionComposition<HostInputArgs<Readonly<Record<never, SceneInput>>>, {
card: Card;
caption: ChildField<typeof Caption, object>;
}>(input: CompositionDefinition<HostInputArgs<Readonly<Record<never, SceneInput>>>, {
card: Card;
caption: ChildField<typeof Caption, object>;
}>): {
id: string;
width: number;
height: number;
fps: number;
defaultProps: HostInputArgs<Readonly<Record<never, SceneInput>>>;
durationInFrames: number;
component: React.FC<...>;
calculateMetadata: CalculateMetadataFunction<...>;
}
A Remotion composition for `input`: like every host, it mounts the runtime media family after the runtime's own renderers.
The mounted instance builds at Remotion's resolved viewport, which `calculateMetadata` or a Player may set apart from the definition's.toRemotionComposition(const card: CompositionDefinition<HostInputArgs<Readonly<Record<never, SceneInput>>>, {
card: Card;
caption: ChildField<typeof Caption, object>;
}, ReactBackendHost, React.ReactNode, React.ReactElement<unknown, string | React.JSXElementConstructor<any>>>
card)} />;
}
function registerRoot(comp: React.FC<{}>): voidregisterRoot(function RemotionRoot(): JSX.ElementRemotionRoot);
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 covers
both.
The view reads the two exports and draws them itself. A composition with reusable actors
draws them with SceneRenderer and registered renderers instead.
What the check asserts
import { const scene: SceneDefinition<Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, {
card: Card;
caption: ChildField<typeof Caption, object>;
}>
scene } from "./scene";
using using wide: CompiledScene<{
card: Card;
caption: ChildField<typeof Caption, object>;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
wide = const scene: SceneDefinition<Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, {
card: Card;
caption: ChildField<typeof Caption, object>;
}>
scene.SceneDefinition<Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, { ...; }>.build(args: SceneBuildInput<Readonly<Record<never, SceneInput>>>): CompiledScene<{
card: Card;
caption: ChildField<typeof Caption, object>;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
build({ viewport: SceneViewportviewport: { SceneViewport.width: numberwidth: 800, SceneViewport.height: numberheight: 450 }, fps: numberfps: 30 });
using wide: CompiledScene<{
card: Card;
caption: ChildField<typeof Caption, object>;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
wide.CompiledScene<{ ...; }, CompiledLayersOf<...>>.at(frame: Frame, options?: FrameResolutionOptions): FrameState<{
card: Card;
caption: ChildField<typeof Caption, object>;
}>
at(15).FrameState<{ ...; }>.scene: {
card: ResolvedActorOfInstance<Card> | undefined;
caption: ResolvedActorOfInstance<Caption> | undefined;
}
scene.card: ResolvedActorOfInstance<Card> | undefinedcard?.progress: number | undefinedprogress; // 0.5
using wide: CompiledScene<{
card: Card;
caption: ChildField<typeof Caption, object>;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
wide.CompiledScene<{ ...; }, CompiledLayersOf<...>>.at(frame: Frame, options?: FrameResolutionOptions): FrameState<{
card: Card;
caption: ChildField<typeof Caption, object>;
}>
at(15).FrameState<{ ...; }>.scene: {
card: ResolvedActorOfInstance<Card> | undefined;
caption: ResolvedActorOfInstance<Caption> | undefined;
}
scene.card: ResolvedActorOfInstance<Card> | undefinedcard?.revealWidth: number | undefinedrevealWidth; // 300
using using narrow: CompiledScene<{
card: Card;
caption: ChildField<typeof Caption, object>;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
narrow = const scene: SceneDefinition<Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, {
card: Card;
caption: ChildField<typeof Caption, object>;
}>
scene.SceneDefinition<Readonly<Record<never, SceneInput>>, Readonly<Record<never, SceneLayer<BackendRef<string>>>>, { ...; }>.build(args: SceneBuildInput<Readonly<Record<never, SceneInput>>>): CompiledScene<{
card: Card;
caption: ChildField<typeof Caption, object>;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
build({ viewport: SceneViewportviewport: { SceneViewport.width: numberwidth: 400, SceneViewport.height: numberheight: 225 }, fps: numberfps: 30 });
using narrow: CompiledScene<{
card: Card;
caption: ChildField<typeof Caption, object>;
}, CompiledLayersOf<Readonly<Record<never, SceneLayer<BackendRef<string>>>>>>
narrow.CompiledScene<{ ...; }, CompiledLayersOf<...>>.at(frame: Frame, options?: FrameResolutionOptions): FrameState<{
card: Card;
caption: ChildField<typeof Caption, object>;
}>
at(15).FrameState<{ ...; }>.scene: {
card: ResolvedActorOfInstance<Card> | undefined;
caption: ResolvedActorOfInstance<Caption> | undefined;
}
scene.card: ResolvedActorOfInstance<Card> | undefinedcard?.revealWidth: number | undefinedrevealWidth; // 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.