Remotion render pitfalls
Free agent skills for marketing, ads & creatives by POPJAM.IO — npx skills add popjam-io/skills
npx -y skills add popjam-io/skills --skill remotion-render-pitfallsAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
POPJAM-specific render-breaking mistakes that fail our renderer (tsc, eslint, and Remotion render). Read this BEFORE writing animation source files. First-party supplement to the external remotion-best-practices skill.
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
13.7 KB, as published. Nobody here has run it
Render-breaking pitfalls (read before writing code)
First-party POPJAM skill. This is maintained by us and is intentionally separate from the externally-maintained
remotion-best-practicesskill so our renderer-specific rules survive upstream syncs. Use it together withremotion-best-practices(general Remotion knowledge) — this skill adds the mistakes that specifically fail POPJAM's pipeline.
POPJAM renders every animation through a strict pipeline: tsc --noEmit →
eslint → Remotion render. If any stage fails, the whole render fails and the
work is wasted. The mistakes below are the ones that actually break renders in
production, ordered by how often they happen. Avoid all of them.
1. Declare or import every identifier (TS2304 "Cannot find name")
Every name you reference must be either imported or defined in the same scope. Recurrent offenders:
- Remotion helpers used without importing them:
interpolate,spring,Easing,useCurrentFrame,useVideoConfig,Sequence,AbsoluteFill,Img,staticFile. - Constants referenced before/without declaration:
colors,COLORS, scene constants likeSCENE_3_WAVEFORM_HEIGHTS.
✅ Import everything from remotion you use, and define every constant you read:
import { useCurrentFrame, useVideoConfig, interpolate, spring, Easing, Sequence, AbsoluteFill, Img, staticFile } from "remotion";
const COLORS = { primary: "#1B8EFB", accent: "#14816E" };
Before calling render_animation, mentally check that no identifier is used
that isn't imported or declared in that file.
2. Google Font module names are PascalCase with NO underscores (TS2307)
import { loadFont } from "@remotion/google-fonts/<Family>" resolves only when
<Family> is the family name concatenated in PascalCase — spaces removed,
never replaced with underscores:
| Font | ✅ Correct module | ❌ Fails TS2307 |
|---|---|---|
| Playfair Display | PlayfairDisplay | Playfair_Display |
| Open Sans | OpenSans | Open_Sans |
| Space Grotesk | SpaceGrotesk | Space_Grotesk |
| Roboto | Roboto | — |
⚠️ Underscore spellings (
Playfair_Display,Space_Grotesk) have been the single most commonTS2307 Cannot find modulefailure in production. There is no installed module with an underscore in its name.
Safe approach:
- Confirmed-working families:
Inter,Roboto,Montserrat,Poppins,OpenSans,Lato,Oswald,PlayfairDisplay,SpaceGrotesk. - If unsure a family is available, use a system font stack instead — never risk a missing module:
const fontFamily = "'Inter', system-ui, -apple-system, Helvetica, Arial, sans-serif";
loadFont() signature (TS2345). The FIRST argument is the style string;
options come second. Passing the options object first fails tsc:
// ❌ WRONG — TS2345
const { fontFamily } = loadFont({ weights: ["400", "700"], subsets: ["latin"] });
// ✅ CORRECT
const { fontFamily } = loadFont("normal", { weights: ["400", "700"], subsets: ["latin"] });
For deeper loadFont() guidance (weights, subsets), see the
remotion-best-practices skill's google-fonts rule.
3. ASCII only in code (TS1127 "Invalid character")
Smart quotes (“ ” ‘ ’), non-breaking spaces, em-dashes pasted into code, and
other non-ASCII characters in source break the TypeScript parser. Use plain
ASCII " and ' in code. (Unicode is fine inside rendered string values
like ad copy, just not in the code syntax itself.)
4. Make render deterministic — avoid "Output file not found after render"
A render that produces no output file usually means the composition threw or hung at render time. Guard against it:
- No top-level throws or side effects. Code at module top level (outside the component) runs during bundling — a throw there aborts the whole render.
- Bake all data into
<Composition defaultProps>. Do not fetch at render time; the renderer has no app network/auth context. Asset URLs must be public HTTPS and already verified. - Pure functions of
frame. NoDate.now(),Math.random()without a seed, timers, or DOM measurement that can vary — non-determinism causes intermittent render failures. - Guard array/object access.
.map()over data baked into props; never index into something that can beundefinedat frame 0.
5. The eslint stage can fail the render
eslint is a stage in the pipeline. Most eslint rules are warnings and
do not block the render, but a few categories do:
IIFEs in JSX — @eslint-react/unsupported-syntax rejects
{(() => { ... })()} style IIFEs ("IIFEs will not be optimized by React
Compiler").
❌ WRONG — fails eslint, fails the render:
return (
<AbsoluteFill>
{(() => {
const n = Math.floor(frame / 5);
return <span>{n}</span>;
})()}
</AbsoluteFill>
);
✅ CORRECT — lift the logic into a const above the return (or a .map() /
small helper component), then reference the result in the markup:
const n = Math.floor(frame / 5);
return (
<AbsoluteFill>
<span>{n}</span>
</AbsoluteFill>
);
This applies to every IIFE form: {(() => {...})()}, {((x) => ...)(y)},
{(function () {...})()}. (A module-scope IIFE like
const config = (() => {...})() is fine — the rule is JSX-specific.)
no-useless-assignment — assigning a value you never read trips the rule.
This usually means a particle/wave/timeline calculation whose result you
forgot to render, or a value you recompute inline instead of using the variable.
❌ const x = p.x + amp; return <div style={{ left: p.x }} />; (x unused)
✅ Either use the variable (left: x) or delete the assignment.
Note on preview parity. The frontend live preview lints with
eslint-plugin-only-warnregistered globally, so the same rule there is a warning and the preview keeps rendering. The renderer's eslint matches this behaviour. Warnings (severity 1) are surfaced back to the caller in the render result for diagnostics, but they do not fail the render — only severity-2 errors do. Treat the warnings as guidance for the next render, not as a reason to re-render this turn.
6. Composition prop typing (avoids TS2322 "Type not assignable")
React.FC on the root scene component must accept a permissive prop shape
(Record<string, unknown>) and cast individual props inside. A typed prop
interface on the root breaks <Composition>'s generic-typed component prop.
Sub-components may use their own typed props freely.
export const MainScene: React.FC<Record<string, unknown>> = (props) => {
const title = (props.title as string) || '';
};
If you see TS2322 errors pointing at the root component's prop type, this is the fix.
7. Font must cover the content's language — language-specific glyphs and casing
A font that renders Latin English fine can still drop or tofu glyphs for
other languages, and more importantly, case-folding rules differ per
language. Picking a font (or applying .toUpperCase()/.toLowerCase())
without checking the language produces broken, unprofessional output:
- Turkish: uppercase
i→İ(U+0130, dotted capital I), notI. LowercaseI→ı(U+0131, dotless i), noti. The charactersİ/ıare distinct code points; most Latin fonts omit them or render a generic fallback. Also checkğ ş ç ö ü. - German: uppercase
ß→ẞ(U+1E9E) orSSdepending on style;ä ö ü. - Nordic:
æ ø å/Æ Ø Å. - Vietnamese:
đ ơ ưand precomposed diacritics that many Latin fonts lack. - Arabic / Hebrew / Cyrillic / CJK: require fonts with the matching script coverage; do not assume a Latin family includes them.
Rules:
- Check the content language before choosing the font. If
loadFont()accepts asubsetsoption, include the matching subset (e.g.latin-ext,vietnamese,cyrillic); if unsure, request all subsets rather than the defaultlatinonly. - Never call
.toUpperCase()/.toLowerCase()blindly — use a language-aware transform (or pass the already-cased string in from the agent) so Turkishi→İandI→ıare honored. JSString.toUpperCasedoes not apply Turkish/Azeri casing rules. - Prefer a known wide-coverage family (
Inter,Noto Sans,Roboto,Open Sans) over display/narrow fonts when the content has diacritics or non-Latin characters.Oswald,Playfair_Display, etc. frequently lacklatin-ext. - When in doubt, system-ui has broad coverage; fall back to it rather than shipping a font that tofus the copy.
This won't fail tsc/eslint, but it produces visibly broken renders that
get rejected downstream.
8. Only scale / translate / rotate exist as standalone style props (TS2353 / TS2561 / TS2783)
CSS has exactly three standalone transform properties: scale, translate,
and rotate. Everything else — scaleX, scaleY, skew, skewX, skewY,
rotateX, rotateY, rotateZ, translateX, translateY — is not a CSS
property and fails tsc when used as a style key ("Object literal may only
specify known properties"). Put those inside a single transform string:
// ❌ WRONG — TS2353/TS2561: 'skewY' / 'scaleX' do not exist in Properties
style={{ skewY: `${tilt}deg`, scaleX: stretch }}
// ✅ CORRECT — one transform string for anything beyond scale/translate/rotate
style={{ transform: `skewY(${tilt}deg) scaleX(${stretch})` }}
// ✅ ALSO CORRECT — the three real standalone props
style={{ scale: String(s), translate: `0px ${y}px`, rotate: `${r}deg` }}
Never set both a standalone prop AND transform, and never specify transform
twice in one object (TS2783 "'transform' is specified more than once") — merge
every function into a single transform string.
9. Remotion hooks only inside components rendered by the Composition
useCurrentFrame() / useVideoConfig() are React hooks. Calling them at
module scope, inside a plain helper function, or inside a .map() callback
that isn't a component crashes the render at runtime
("useCurrentFrame() can only be called inside a component that was rendered by
Remotion"). tsc and eslint do NOT catch every case — the render just dies.
// ❌ WRONG — module scope
const frame = useCurrentFrame();
// ❌ WRONG — plain helper invoked from JSX
const barHeight = (i: number) => interpolate(useCurrentFrame(), [0, 30], [0, i]);
// ✅ CORRECT — call the hook ONCE at the top of the component, pass the value down
const Scene: React.FC<Record<string, unknown>> = () => {
const frame = useCurrentFrame();
const barHeight = (i: number) => interpolate(frame, [0, 30], [0, i]);
...
};
10. Inline styles only — no Tailwind / className utilities
Style every element with inline style={{ ... }} objects. The server renderer
happens to process Tailwind classes, but the in-app live preview does NOT — a
className="flex items-center gap-4" creative looks correct in the final MP4
while the user's live preview shows unstyled content. Inline styles render
identically in both. (Using className as a semantic hook is fine; just never
rely on it for styling.)
11. <Composition> numeric props must be literal numbers
width, height, fps, and durationInFrames on the <Composition> element
must be written as literal integers (durationInFrames={450}), never
constants or expressions (durationInFrames={TOTAL_FRAMES},
durationInFrames={15 * 30}). POPJAM tooling statically parses these values to
drive the frontend live-preview Player and stored metadata — an expression
parses as the default (300 frames / 1080x1920) and the preview plays the wrong
duration even though the MP4 renders fine. Compute the number yourself and
write the result.
Pre-render self-check (run through this before render_animation)
- Every identifier is imported or declared (no stray
colors,interpolate…). - Every
@remotion/google-fonts/*import is PascalCase with NO underscores (PlayfairDisplay, notPlayfair_Display) and a real installed family, else system stack;loadFont("normal", {...})— style string first. - Code is ASCII-only (smart quotes live only inside rendered strings).
- No top-level throws; all data baked into
defaultProps; render is pure. - No IIFEs in JSX — lift inline logic into a
const/helper abovereturn; every variable you compute is actually read (nono-useless-assignment). - Root scene component is typed
React.FC<Record<string, unknown>>and casts props inside. - Font covers the content's language: matching
subsetsrequested, language- aware casing (no blind.toUpperCase()for Turkishİ/ı), fallback to a wide-coverage family orsystem-uiif unsure. - Transforms: only
scale/translate/rotateas standalone style props —skewY,scaleX, etc. go inside ONEtransformstring, never duplicated. useCurrentFrame()/useVideoConfig()called only at the top of components, never module scope or plain helpers.- All styling is inline
style={{ ... }}— no Tailwind/className utilities (the live preview doesn't process them; the render does — they'd diverge). <Composition>width/height/fps/durationInFramesare literal integers, not constants or expressions.