Verify AI-Generated Remotion Code Before You Render
By RenderComp Team Editorial policy
Remotion turns React code into a video file. Every frame is a deterministic render of a React tree at a given frame index, so animation bugs are not random. They are reproducible and catchable before the encoder runs. That property becomes critical when an AI agent writes the composition: the same determinism that makes Remotion reliable also means a wrong parameter bakes a wrong frame into every copy of the exported file.
Agents generating Remotion code fail in the same four places. They hardcode frame counts as if fps were constant across all compositions. They call interpolate without clamping. They pick spring configs whose settle time exceeds the composition’s duration. They pass seconds instead of frames to Sequence’s from prop, which shifts the child timeline in ways the agent did not account for. None of these produce a compile error; the composition starts without crashing, and the problem surfaces only in the encoded output.
The checks below address each failure mode with the Remotion APIs that expose the underlying values directly.
Frame arithmetic must use fps from useVideoConfig
Remotion measures everything in frames, not seconds. A composition declared as durationInFrames: 150 runs for 5 seconds at 30 fps and 6.25 seconds at 24 fps. AI agents frequently hardcode a numeric frame count as if fps were a universal constant, producing compositions that run at the correct pace in the project that trained the agent and at the wrong pace everywhere else.
Every timing value should be derived from useVideoConfig(), not from a literal.
import { useVideoConfig, useCurrentFrame, interpolate, AbsoluteFill } from 'remotion';
export const Slide: React.FC = () => {
const { fps, durationInFrames } = useVideoConfig();
const frame = useCurrentFrame();
// Fade in over the first 0.5 s, regardless of the composition's fps setting.
// At 30fps this is frames 0-15; at 24fps it is frames 0-12.
const fadeIn = interpolate(
frame,
[0, fps * 0.5],
[0, 1],
{ extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }
);
// Fade out over the last 1 second, regardless of total frame count
const fadeOut = interpolate(
frame,
[durationInFrames - fps, durationInFrames],
[1, 0],
{ extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }
);
return (
<AbsoluteFill style={{ opacity: fadeIn * fadeOut }}>
{/* slide content */}
</AbsoluteFill>
);
};
When reviewing AI-generated code, scan for numeric literals passed directly to an interpolate input range. Any number that is not derived from fps or durationInFrames is a hardcoded assumption that breaks when the composition runs at a different frame rate.
interpolate without clamping extrapolates past the output range
interpolate extrapolates linearly beyond its declared input range by default. For an opacity animation mapped to [0, 1], passing a frame past the upper bound produces values above 1.0. The browser compositor silently clips opacity during Preview playback, so the animation looks correct in Remotion Studio. The encoded output does not always suppress the overshoot at the compositing stage, and non-opacity properties such as translateX or scale continue past their intended stop with no clipping at all.
import { interpolate, useCurrentFrame } from 'remotion';
export const BadSlide: React.FC = () => {
const frame = useCurrentFrame();
// At frame 14, the output is 1.4, not 1.0.
// Looks finished in Studio; encoder sees an out-of-range value.
const opacity = interpolate(frame, [0, 10], [0, 1]);
return <div style={{ opacity }}>...</div>;
};
export const CorrectSlide: React.FC = () => {
const frame = useCurrentFrame();
const opacity = interpolate(
frame,
[0, 10], // input range in frames
[0, 1], // output range
{
extrapolateLeft: 'clamp', // floor at 0 before frame 0
extrapolateRight: 'clamp', // ceiling at 1 after frame 10
}
);
return <div style={{ opacity }}>...</div>;
};
Check every interpolate call for an explicit extrapolateRight option. If the input range ends before durationInFrames, 'clamp' is the correct choice in almost every case.
spring configs must settle before the composition ends
spring() has no fixed duration. It runs until the output settles within a tolerance of its target, and that settle time depends on stiffness, damping, and mass. Remotion’s defaults are stiffness: 100, damping: 10, mass: 1. Agents that increase stiffness to produce a snappier feel without adjusting damping introduce overshoot that can take far longer to decay than the animation’s visible portion.
In a 60-frame composition with a high-stiffness, low-damping spring, the element is still oscillating when the video ends. measureSpring from remotion returns the exact frame count before settling, so you can assert it against durationInFrames before queuing a render.
import { measureSpring } from 'remotion';
const FPS = 30;
const DURATION = 60; // frames
// AI-generated config: high stiffness, default damping
const config = { stiffness: 300, damping: 10 };
const settlesAt = measureSpring({
fps: FPS,
config,
threshold: 0.005, // accept within 0.5% of target
});
if (settlesAt > DURATION) {
throw new Error(
`Spring settles at frame ${settlesAt}, but composition ends at ${DURATION}. ` +
`Raise damping or reduce stiffness.`
);
}
The threshold of 0.005 matches Remotion’s internal tolerance for considering a spring visually settled. Tightening it to 0.001 reveals configs that linger beyond what measureSpring would pass at the looser threshold.
Sequence shifts useCurrentFrame and scopes durationInFrames
Sequence clips its children to a window of frames. The from prop shifts the child timeline: inside <Sequence from={30}>, useCurrentFrame() returns 0 when the parent composition is at frame 30. Children are not rendered before that frame. When a durationInFrames prop is set on the Sequence, useVideoConfig().durationInFrames inside the child returns the Sequence’s own duration, not the outer composition’s.
Agents make two mistakes here. The first is passing a seconds value to from. The second is calculating fade timings against what the agent assumes is the full composition length, when the returned durationInFrames is actually the Sequence’s shorter window.
import { Sequence, useVideoConfig, useCurrentFrame } from 'remotion';
// The child sees durationInFrames = 60, not the outer composition's 150.
export const Outer: React.FC = () => {
const { fps } = useVideoConfig();
return (
// fps * 1 = frame 30 at 30fps, frame 24 at 24fps
<Sequence from={fps * 1} durationInFrames={60}>
<Inner />
</Sequence>
);
};
const Inner: React.FC = () => {
const { durationInFrames } = useVideoConfig();
const frame = useCurrentFrame();
// durationInFrames is 60 here, not 150.
// An agent that assumed 150 places fadeOutStart at frame 140,
// which is past the Sequence's end — the fade never renders.
const fadeOutStart = durationInFrames - 10; // 50, correct for this window
const opacity = interpolate(
frame,
[fadeOutStart, durationInFrames],
[1, 0],
{ extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }
);
return <div style={{ opacity }}>content</div>;
};
When a fade that looks correct in Preview is absent from the encoded file, check whether durationInFrames is being read inside a Sequence that overrides it.
A verification script to run before every render
The frame-arithmetic check is a code review pass. The interpolate and spring checks can be automated. Grouping spring configs into a validation function and running it in CI catches the timing mismatch without opening Remotion Studio.
// scripts/verify-compositions.ts
import { measureSpring } from 'remotion';
type SpringConfig = { stiffness: number; damping: number; mass?: number };
function verifySpring(
label: string,
config: SpringConfig,
fps: number,
durationInFrames: number,
threshold = 0.005
) {
const settles = measureSpring({ fps, config, threshold });
if (settles > durationInFrames) {
throw new Error(
`[${label}] Spring settles at frame ${settles}, composition ends at ${durationInFrames}. ` +
`Adjust stiffness or damping.`
);
}
console.log(`[${label}] OK — settles at frame ${settles} of ${durationInFrames}`);
}
// One call per spring config in the generated compositions
verifySpring('title-entrance', { stiffness: 100, damping: 10 }, 30, 90);
verifySpring('chart-bar-rise', { stiffness: 200, damping: 14 }, 30, 60);
verifySpring('logo-pop', { stiffness: 300, damping: 20 }, 30, 45);
Run this with npx ts-node scripts/verify-compositions.ts as a CI step before the step that calls npx remotion render. An agent that generates new compositions can be instructed to append a verifySpring call for each spring config it produces. The verification then scales with generated output rather than depending on a manual review cycle.
Now available
Get 1,000+ Remotion Templates
Pay once — no subscription. Lifetime updates. TypeScript-first.
View pricing →