How to setup Remotion Composition?
Setting up a composition in Remotion can feel overwhelming, especially when you consider all the components involved. From defining strict types with TypeScript to incorporating Zod types for validation, and adding functions like calculateMetadata
, there’s a lot to keep track of.
It can be daunting to ensure that each part works seamlessly together, especially when combining complex types in TypeScript. We will break down each step to help you set up your composition in an organized and type-safe way, making the process less intimidating and more manageable.
Starting out from a template
The simplest way to get started is by using the official Remotion "helloworld" template. This template provides a ready-made setup, allowing you to experiment without needing to create all the essential files from scratch. In this article, we’ll walk you through the setup, starting with Root.tsx
, where the key rendering process begins.
The structure of a Remotion Composition
A <Composition />
element element requires several key properties to be specified.
At its core, you'll need to define an id (used for referencing the composition when rendering), along with standard video properties like fps
, width
, height
, and durationInFrames
.
In our examples, we often refer to the component as Scene
, as this describes its purpose - the entire visual sequence starts from this component.
// A simple scene to render
const Scene: React.FC = () => {
const frame = useCurrentFrame()
// Very ugly output, doesn't matter
return <p>Frame {frame}</p>
}
const MyComposition = () => {
return (
<Composition
// Required properties
id="MyComposition"
width={1280}
height={720}
fps={30}
// 3 seconds at 30 fps
durationInFrames={90}
component={Scene}
/>
)
}
Adding schema property to the Remotion Composition
To incorporate custom types into your scene, you need to define the expected properties using Zod types and pass them to the Composition
as the schema
property. These Zod types must align with the types in your Scene
; otherwise, you'll encounter a compile-time error.
If you're new to TypeScript and just want to render your first video, you might wonder why we use Zod types instead of regular TypeScript types. Here's the difference:
Instead of writing:
type SceneProps = {
circleCount: number
}
You would use Zod like this:
const schema = z.object({
circleCount: z.number(),
})
type SceneProps = z.infer<typeof schema>
In TypeScript, types exist only at compile time and are not carried over to runtime, which means they can't be used to dynamically enforce or validate data types. This is where Zod comes in - it allows you to define a schema that persists at runtime, enabling validation and type-checking even after compilation. In Remotion, using Zod schemas not only ensures that your data is correctly typed but also allows Remotion Studio to automatically generate UI controls for those properties, making it easier to fine-tune your compositions without directly modifying the code.
This is how the <Composition />
is modified to include a custom schema
:
const Scene: React.FC<SceneProps> = (props) => {
return (
<p>Circle count is {props.circleCount}</p>
)
}
export const MyComposition = () => {
return (
<Composition
id="MyComposition"
width={1280}
height={720}
fps={30}
durationInFrames={90}
component={Scene}
// Pass the schema instance
schema={schema}
defaultProps={{
// Set the required properties
circleCount: 5,
}}
/>
)
}
Using calculateMetadata function
The calculateMetadata
function is useful for adjusting <Composition />
attributes based calculated values. For example, in the typography template, it's used to calculate the durationInFrames
property by calculating the length of a text-to-speech audio file. An example of this calculation looks like:
const calculateMyMetadata: CalculateMetadataFunction<
SceneProps
> = async ({ props }) => {
// The fps specified in Composition
// is not directly accessible here.
const fps = 60
const seconds =
await calculateAudioSeconds(props)
return {
// Provide the fps, so durationInFrames
// is accurate.
fps,
// Round up the value
durationInFrames: Math.ceil(
fps * seconds,
),
}
}
the Composition
will look like this in the Remotion root
. Please keep in mind that the result of the calculateMetadata
takes precedence over the properties defined in the Composition
, so the fps
and durationInFrames
values will be overwritten.
export const MyComposition = () => {
return (
<Composition
id="MyComposition"
width={1280}
height={720}
// fps and durationInFrames
// will be overridden
fps={30}
durationInFrames={100}
component={Scene}
schema={schema}
// Set the calculateMetadata
calculateMetadata={
calculateMyMetadata
}
defaultProps={{
circleCount: 5,
}}
/>
)
}
Calculating additional props in the calculateMetadata function
Another use case for calculateMetadata
is to perform calculations and modify the provided props. In the previous example, adding calculateMetadata
to the Composition
was straightforward because of its function signature:
const calculateMyMetadata = CalculateMetadataFunction<
SceneProps
> // ... rest of the code
This signature indicates that it expects SceneProps
as input
and also returns SceneProps
. (If it doesn’t return new props in its return
statement, the input remains unchanged.)
But what if we need to perform some calculations and put the result of said calculation in a field of the props? How do we specify the type in such cases?
Let’s explore adding an extra calculated
field to props
, building on the existing SceneProps
, to store the result of our calculations:
type CalculateMyMetadataProps = SceneProps & {
// Placeholder for additional calculated data
calculated: {
favouriteNumber: number
}
}
const calculateMyMetadata: CalculateMetadataFunction<
CalculateMyMetadataProps
> = async ({ props }) => {
const fps = 60
const seconds =
await calculateAudioSeconds(props)
return {
fps,
durationInFrames: Math.ceil(
fps * seconds,
),
props: {
...props,
calculated: {
favouriteNumber:
// Perform some calculation
props.circleCount ** 2,
},
},
}
}
While this approach is correct, it introduces a problem: an error appears when defining the <Composition />
. The schema
and defaultProps
must now match the updated props structure.
<Composition
// ... rest of the code
// Error:
// "Types of parameters options and options are incompatible."
calculateMetadata={
calculateMyMetadata
}
/>
While we could provide some kind of dummy data in the defaultProps
or update the schema
to specify a calculated
field, it is not only a complex solution, but also misleading. (The calculated
field should only be defined with a valid value.) The most straight-forward way to resolve this problem is by making the calculated
field optional. That way, the defaultProps
does not have to specify it, and the schema
matches with the required fields:
type CalculateMyMetadataProps = SceneProps & {
// Make it optional
calculated?: {
favouriteNumber: number
}
}
Now the error mentioned earlier is gone.
Next, we can update the Scene
component to account for the optional calculated
field. In practice, this field will always be populated when rendering the scene, since the rendering process doesn’t start until calculateMetadata
has completed. Therefore, it is safe to access the value even though the type indicates it could be undefined.
Here's the updated code for the Scene
:
const Scene: React.FC<
CalculateMyMetadataProps
> = (props) => {
const { calculated } = props
// Add a guard so nullability check
// is done once.
if (!calculated) {
throw new Error(
"Did you specify " +
"the calculateMetadata correctly?",
)
}
return (
<p>
Favorite number is{" "}
{calculated.favouriteNumber}
</p>
)
}
Here is the complete code for the example:
async function calculateAudioSeconds(
props: SceneProps,
) {
// Fetch from server, etc ...
return 10
}
const schema = z.object({
circleCount: z.number(),
})
type Schema = typeof schema
type SceneProps = z.infer<Schema>
const Scene: React.FC<
CalculateMyMetadataProps
> = (props) => {
const { calculated } = props
if (!calculated) {
throw new Error(
"Did you specify " +
"the calculateMetadata correctly?",
)
}
return (
<p>
Favorite number is{" "}
{calculated.favouriteNumber}
</p>
)
}
type CalculateMyMetadataProps = SceneProps & {
calculated?: {
favouriteNumber: number
}
}
const calculateMyMetadata: CalculateMetadataFunction<
CalculateMyMetadataProps
> = async ({ props }) => {
const fps = 60
const seconds =
await calculateAudioSeconds(props)
return {
fps,
durationInFrames: Math.ceil(
fps * seconds,
),
props: {
...props,
calculated: {
favouriteNumber:
props.circleCount ** 2,
},
},
}
}
export const MyComposition = () => {
return (
<Composition
id="MyComposition"
width={1280}
height={720}
fps={30}
durationInFrames={100}
component={Scene}
schema={schema}
calculateMetadata={
calculateMyMetadata
}
defaultProps={{
circleCount: 5,
}}
/>
)
}
Then the MyComposition
can simply be rendered in the Root
.
Summary
In this article, we explored how to set up a Remotion composition effectively, focusing on using Zod for type validation and the calculateMetadata
function to dynamically adjust composition properties. We discussed how to handle additional calculated props, how to make them optional to avoid errors, and how to update components to safely access these values. The final example demonstrated how to use these concepts to create a flexible and type-safe animation in Remotion.
If you've made it this far in setting up the project, it's the perfect time to explore the Remotion animations intro.