I wanted to turn an article about Payload CMS 4.0 into a short animated teaser. The content already had useful visual ideas: an access-control comparison, a relationship-depth diagram, and two framework adapters sharing one core. Remotion let me turn those ideas into video using React components, TypeScript, CSS, and SVG.
Here is the finished 15-second video we will recreate:
By the end, you will have the same five-scene video in two formats: 1920×1080 landscape and 1080×1080 square. You will also understand how Remotion connects a frame number to what appears on screen, how scenes get their own timelines, and how to make fonts and images ready before a frame is captured.
You should be comfortable with React components, hooks, TypeScript, and basic CSS. You do not need a Payload installation: all the Payload content in this example is text and graphics. The canary labels describe the example's subject; this tutorial is about creating its animation.
This is a complete implementation walkthrough. Each application source file appears below. The large component is split into consecutive blocks so we can discuss each part. Copy those blocks into the same file in the order shown.
1. Understand what Remotion renders
Slide Gallery· remotion-pipeline-scenes
1 / 4
UI = f(frame): The Mental Model: Remotion doesn't use timelines or keyframe tracks; every frame is a deterministic React render at frame N.
Think of a composition as a React component with a video-sized canvas and a timeline. Remotion supplies the current frame. Your component calculates positions, opacity, scale, and visible content for that frame.
At 60 frames per second, frame 60 represents one second into the video. Our composition lasts 900 frames, so its duration is 900 / 60 = 15 seconds. Frames are zero-based: the first is 0 and the last is 899.
In Studio, you can scrub directly to a frame and see the component at that point. During a local render, Remotion uses a browser to capture frames and encodes them into a video. The component must therefore produce the right visual even when you jump directly to a later frame.
This changes how you animate. A web page often starts a CSS transition after a click and lets time pass. In a Remotion composition, the current frame determines the animation state. There is no need to play the previous 100 frames to know how frame 101 should look. Remotion explicitly recommends frame-driven animation instead of CSS transitions and keyframes. Read the animation guidance.
Our implementation uses two main tools: interpolate() for mapping progress to visual values, and spring() for entrances with a natural settling motion. Ordinary CSS still handles layout, typography, borders, gradients, and colors.
2. Create an isolated video project
Use Node.js 24 and pnpm 10.33.0 for this walkthrough. Create a fresh directory outside any existing pnpm workspace, so its dependencies and configuration belong to this example:
bash
mkdir remotion-payload-teaser
cd remotion-payload-teaser
mkdir -p src public/fonts out
All following shell commands run from this directory. Create the package file before installing anything. These are the versions used by the reference implementation; keep the Remotion packages on the same exact version.
The first install generates pnpm-lock.yaml. Keep that lockfile with your project. Once it exists, use pnpm install --frozen-lockfile for repeat installations. You do not need to hand-write or copy hundreds of lockfile entries from an article.
React and React DOM render our components. Remotion supplies the timeline primitives. The CLI opens Studio and exports media. Lucide provides SVG icons, the Fontsource packages supply local font files, and the Tailwind integration processes the utility classes used by the layout. Zod is pinned in the reference package; this composition does not define a custom props schema.
The TypeScript configuration includes browser types because the font loader uses FontFace and document.fonts. This package is independent of any website's TypeScript configuration.
Tailwind v4 needs a bundler integration in this project. Installing Tailwind alone does not make the classes in our components work. Use Remotion's enableTailwind() override. The overwrite setting allows subsequent exports to replace the same output filename. See the official integration.
Download the example logo and author portrait into public/, keeping their filenames. These are the assets shown in the video. For your own video, replace them with your branding.
On macOS or Linux, you can download them from the site with:
Copy the font files from the packages you just installed, and rename them to match the paths our component will request:
bash
for weight in 400 500 600 700; docp"node_modules/@fontsource/inter/files/inter-latin-${weight}-normal.woff2""public/fonts/inter-${weight}.woff2"donecp node_modules/@fontsource/ibm-plex-mono/files/ibm-plex-mono-latin-400-normal.woff2 public/fonts/plex-mono-400.woff2
cp node_modules/@fontsource/inter/LICENSE public/fonts/inter-LICENSE.txt
cp node_modules/@fontsource/ibm-plex-mono/LICENSE public/fonts/ibm-plex-mono-LICENSE.txt
The Latin subset covers the example's text. If you adapt it to another writing system, select a font subset that includes those characters.
Remotion's staticFile() resolves assets from the project's public/ directory. We will use it for both the images and fonts, rather than relying on a website-relative URL. See staticFile().
3. Register the entrypoint and compositions
The CLI needs an entrypoint that registers our root component.
The root registers two compositions. Each points to the same React component, but supplies different canvas dimensions. The IDs are what the render scripts select. See the composition API.
Both compositions run at 60 fps for 900 frames. The square video is a different layout of the same content, rather than a center crop of the landscape export.
defaultProps supplies the title for the opening scene and the article slug for the closing URL. These props do not make every scene generic: the access-control diagrams and other Payload-specific text remain in the component.
Root.tsx imports Payload4Teaser, which we create next. Wait until the final component is assembled before opening Studio.
4. Create the shared animation and layout components
Create src/Payload4Teaser.tsx. Copy every TSX block from this section through section 6 into this one file, in order. Each block adds new declarations; it does not replace the previous block. Together they are the complete source.
Imports, colors, and layout detection
Start the file with all imports and the shared constants:
C centralizes the palette. mono gives code and labels a consistent font. clamp prevents interpolation from continuing beyond its intended endpoints. The useSquare() hook reads the composition dimensions; it does not use the browser window's size.
That distinction matters when the same composition is previewed at different zoom levels. A 1080×1080 canvas stays square even if Studio shows it inside a wide browser window.
The gate creates a render-delay handle once for the mounted component. It loads the required weights, registers them with the browser, and clears the handle when all fonts are ready. A load failure calls cancelRender, so an export fails rather than silently capturing fallback typography. See the render-delay lifecycle.
The component renders nothing visible. Its purpose is to coordinate readiness. Loading the typefaces matters because fallback fonts can change text widths, line breaks, and diagram spacing.
The cleanup flag prevents a completed request from continuing a render after this instance has unmounted. For a larger application, Remotion also provides font-loading utilities; here we retain the small FontFace implementation used by the reference video. Read the font guide.
Pop combines two drivers. A spring moves the element from 28 pixels below its resting position and scales it from roughly 0.97 toward 1. Interpolation controls opacity. delay shifts both calculations, allowing a heading, subtitle, and badges to enter in succession.
The spring receives the composition's fps; the damping, stiffness, and mass values tune how it settles. The frame passed to it is frame - delay, so each element uses the same entrance with a different start. See the spring API.
There is a subtle detail in the opacity curve: it maps [-8, 16] to [0, 1]. That starts fading eight frames before the spring's nominal start and reaches full opacity sixteen frames after it. delay therefore does not mean completely invisible until that exact frame.
Clamping keeps opacity within the chosen range before and after the animation interval. Without clamping, interpolation continues along the line beyond its endpoints. See interpolation and extrapolation.
AbsoluteFill covers the composition. The wrapper supplies padding, a short entrance, and a short exit based on the scene's local frame. Its default duration is 180 frames; the longer access scene and shorter depth scene pass explicit durations.
This example fades between opacity 0.35 and 1, rather than fading completely to black. The final scene uses hold to keep its end state visible. These are design choices you can change by adjusting the output ranges.
The heading changes font sizes and spacing for the square composition. ReactNode lets subtitles contain either plain text or JSX, such as a highlighted value.
The gradient, grid, rings, and progress bar remain behind every scene. Their movement comes from the current frame. The grid translates using a repeating calculation, and the large ring slowly scales.
The progress bar uses (frame + 1) / 900, so it reaches full width on the last frame, 899. The denominator is deliberately tied to this video's fixed duration. If you change the composition length, update it too, or derive it from useVideoConfig().
The chapter labels switch at the same frame boundaries we will use for the scenes. The logo, canary badge, chapter label, and website address stay visible across the whole timeline.
We will place Background and Brand outside the sequences. They therefore receive the composition's frame, while the scene components inside a sequence receive a local frame. That lets the background stay continuous while each scene starts its own entrance at zero.
5. Build the five scenes
Continue appending these blocks to src/Payload4Teaser.tsx. We are now composing the shared primitives into the exact content shown in the video.
The title is split at its first colon. The first part becomes the headline, and the rest becomes the subtitle. The highlighted 4.0 and the supporting labels are specific to this example.
The tags use delays of 32 + i * 8 frames, so each starts eight frames after the previous tag. At 60 fps that spacing is about 0.13 seconds. The animation stays deterministic because the delay comes from the array index.
The two cards share one component. The v4 flag changes the labels, colors, icons, and connector timing. For the animated SVG line, pathLength={1} normalizes the path length and strokeDashoffset={1 - draw} reveals it as draw moves toward 1.
The square layout stacks the cards and moves the access-default text into each header. That removes repeated detail from the taller layout and keeps room for the query, heading, and footer. Simply changing the canvas size would not make the landscape scene fit.
The scene lasts 240 frames, which is four seconds. Its card entrances use local frame values, so their delays are measured from the start of this scene, not from frame zero of the full video.
The example's message distinguishes access checks being evaluated from a request being granted. The green card still lists Allow, Filter, and Deny; the diagram should not imply that evaluating rules always allows access.
This scene uses two rows to compare the diagrams. The last node is a populated document in one row and an ID in the other. Dashed styling carries that distinction even without relying on color.
The node contents are React components, including Lucide SVG icons. The connectors are SVG paths. Remotion can animate them because their visible properties are calculated from frames, just like the earlier HTML cards.
Scene duration={120} keeps this scene's entrance and exit aligned with its two-second sequence. The second row and explanatory note arrive later through Pop delays.
Both framework cards point toward one Payload core. The Next.js path is solid, while the TanStack Start card and path retain the example's experimental-canary styling.
The solid connector uses the same normalized path-length technique as the access cards. The dashed connector fades in through opacity={progress}. The SVG view box provides a shared coordinate system while its rendered width follows the composition.
The scene is ordinary React composition: two cards, a connector diagram, a core card, and an admin-style note. The motion is concentrated in entrances and connector reveals, making the content easier to read during its three seconds on screen.
The closing URL uses the slug prop. Its square variant puts the path on a second line and wraps the heading deliberately. The portrait uses Remotion's Img component with staticFile(), and the fixed display dimensions keep the surrounding layout stable.
Scene hold keeps the completed CTA at full opacity. Its small scale change settles during the first 48 frames, leaving time to read the URL before the video ends.
6. Assemble the timeline
Append the final exported component. This completes src/Payload4Teaser.tsx:
The order of the layers matters: font coordination first, then the background, then the timed scenes, then branding above them.
Here is the full frame budget:
Scene
Start frame
Duration
Last frame
Time interval
Hook and title
0
180
179
0–3 seconds
Access defaults
180
240
419
3–7 seconds
Relationship depth
420
120
539
7–9 seconds
Frameworks and admin
540
180
719
9–12 seconds
Call to action
720
180
899
12–15 seconds
A sequence's durationInFrames is a count. The first scene includes frames 0 through 179; frame 180 belongs to the next scene. These five intervals cover all 900 frames without a gap or overlap.
Inside <Sequence from={180}>, useCurrentFrame() returns 0 at composition frame 180, 1 at frame 181, and so on. That is why the access cards can use small delays such as 12 and 42: they are relative to that scene. See sequence timing.
The background and brand are outside those sequences and keep the global frame number. Moving them inside a scene would reset their timing when the scene starts.
A sequence also controls when its children appear. You do not need to hide every scene manually with conditions; the sequence supplies the visibility window and shifted timeline.
7. Preview the result in Studio
Check TypeScript and start Studio:
bash
npx tsc --noEmit
pnpm start
Open http://localhost:3100 and select Payload4Teaser. Play the composition, then switch to Payload4Square. If the port is busy, use:
bash
pnpm exec remotion studio src/index.ts --port=3103
Use representative frames to inspect each scene: 150, 340, 490, 660, and 850. Also inspect both sides of every boundary: 179/180, 419/420, 539/540, and 719/720. Check the opening frame 0 and the final frame 899.
At each stop, check whether headings fit, connectors point to the intended nodes, and the persistent footer stays clear. Preview the square composition around social-feed display sizes as well as at full resolution. Large export dimensions do not guarantee readable text on a phone.
You can save an individual frame for a closer look:
bash
pnpm exec remotion still src/index.ts Payload4Square out/check-square-access.png --frame=340
Do not judge a composition only by one attractive poster. The frame before a scene change and the first frame after it reveal timing mistakes that a settled frame can hide.
8. Render MP4, WebM, and posters
The scripts in package.json already specify the entrypoint, composition ID, and output filename. Run them from the example project:
The first render may download Chrome Headless Shell. Let that finish before diagnosing a slow first export as an animation problem. See the rendering walkthrough.
The MP4 scripts use H.264, yuv420p, CRF 18, and BT.709 color space. PNG frame capture avoids introducing JPEG artifacts before encoding around text and thin diagram strokes. --concurrency=4 is the reference setting; reduce it if your machine struggles with memory. The WebM script uses VP9 at a target bitrate of 6 Mbps. See the CLI options.
Posters come from frame 150, 2.5 seconds into the composition, when the opening scene has settled. You can choose another frame by changing --frame in the poster scripts.
Expect these outputs:
Output
Canvas
Purpose
out/payload-4-0-teaser.mp4
1920×1080
Landscape H.264 video
out/payload-4-0-square.mp4
1080×1080
Square H.264 video
out/payload-4-0-teaser.webm
1920×1080
Landscape VP9 alternative
out/payload-4-0-poster.png
1920×1080
Landscape poster
out/payload-4-0-square-poster.png
1080×1080
Square poster
Both video compositions are 15 seconds at 60 fps. This example has no audio track; silent playback is expected.
Here is the square reference export for comparison:
If you have ffprobe installed, inspect the actual encoded media rather than relying only on composition settings:
For the landscape MP4, expect H.264, 1920×1080, 60/1 fps, 900 decoded frames, approximately 15 seconds, yuv420p, and bt709. Check the square file separately; it should report 1080×1080.
Play the final files and seek into each scene. Studio confirms the React composition, while playback confirms the file you will actually share.
9. Adapt the example to your own content
Start by changing the title and slug in both compositions. The title controls the opening heading and subtitle; the slug controls the closing article path. Then change the scene content, logo, portrait, and brand labels.
There are several deliberate fixed values to revisit:
Hook always renders a highlighted 4.0, and its tags refer to Payload's ecosystem.
AccessCard, RelationshipRow, and Frameworks contain example-specific diagrams and labels.
Brand contains fixed chapter boundaries and website text.
Background calculates progress against 900 frames.
useSquare() recognizes a square canvas. A vertical 1080×1920 composition needs its own layout decisions.
Changing defaultProps alone does not turn this into a general-purpose video generator. To make it reusable across articles, extract the scene content into typed props after you have a second real example and know what varies.
If you change the duration, update the composition, sequence starts and lengths, Scene durations, chapter thresholds, and progress denominator together. A duration change at only the root will either cut content short or leave extra time after the intended ending.
Keep animated values derived from frames. If you add particles or randomly placed decorations, use Remotion's seeded random() rather than Math.random() so the placement is repeatable. See deterministic randomness.
10. Troubleshoot the common failures
A component or import cannot be found
Make sure you copied every consecutive block into src/Payload4Teaser.tsx, including the imports at the top and exported component at the bottom. The blocks are parts of one file. Copying only an individual scene will leave its helpers undefined.
Run npx tsc --noEmit from the example directory. If the error mentions FontFace or document.fonts, compare the TypeScript lib array with the configuration above.
Utility classes have no effect
Check that the component imports ./style.css, that the stylesheet imports Tailwind, and that remotion.config.ts calls enableTailwind. Our layout mixes utility classes and inline styles; missing Tailwind can break flex layouts even while colors still appear correctly.
Fonts fail to load or the render times out
Check all five requested font filenames, their copied paths, and the spelling of public/fonts. Use Studio's browser console to look for failed asset requests. The font gate intentionally cancels on a loading error.
A timeout can indicate work that never cleared its delay handle. Increasing the timeout does not repair a missing file or a stalled request. Trace the operation and make sure it either completes with continueRender() or fails with cancelRender(). See render-delay troubleshooting.
Animation looks different after scrubbing
Look for CSS keyframes, transitions, timers, or values calculated from wall-clock time. A composition must display the intended result when you jump straight to a frame. Calculate motion from useCurrentFrame() and keep your asset and input data stable.
Square content clips or collides with the footer
Inspect the entire scene's height, including headings, margins, query text, and notes. The reference square layout stacks access cards, tightens their spacing, and moves detail into their headers. Smaller fonts alone will not fix every layout problem.
The final export is unreadable or fails
Check available disk space and try reducing render concurrency. Inspect the actual output with a media player and ffprobe. A successful TypeScript check confirms types, not that the browser captured every frame correctly.
Where to go after this tutorial
You now have a concrete composition you can scrub, modify, and render. Try replacing the access comparison with a diagram from your own article while keeping the same timing and shared animation helpers.
For a next project, the Remotion Player lets you embed a composition as an interactive preview in a React application. The Node rendering API and Lambda documentation cover automated exports when local rendering is no longer enough. Check the current licensing terms when planning how to use it in your organization.
Build one clear scene, inspect its frames, and then extend the timeline. The same React composition skills carry through as the video grows.