agentsclimarketplace

Remotion render pitfalls

Skill popjam-io/skills/skills/remotion-render-pitfalls

Free agent skills for marketing, ads & creatives by POPJAM.IO — npx skills add popjam-io/skills

Install
npx -y skills add popjam-io/skills --skill remotion-render-pitfalls

Assembled 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-practices skill so our renderer-specific rules survive upstream syncs. Use it together with remotion-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 --noEmiteslint → 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 like SCENE_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 DisplayPlayfairDisplayPlayfair_Display
Open SansOpenSansOpen_Sans
Space GroteskSpaceGroteskSpace_Grotesk
RobotoRoboto

⚠️ Underscore spellings (Playfair_Display, Space_Grotesk) have been the single most common TS2307 Cannot find module failure 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. No Date.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 be undefined at 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-warn registered 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), not I. Lowercase Iı (U+0131, dotless i), not i. The characters İ/ı are distinct code points; most Latin fonts omit them or render a generic fallback. Also check ğ ş ç ö ü.
  • German: uppercase ß (U+1E9E) or SS depending 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:

  1. Check the content language before choosing the font. If loadFont() accepts a subsets option, include the matching subset (e.g. latin-ext, vietnamese, cyrillic); if unsure, request all subsets rather than the default latin only.
  2. Never call .toUpperCase() / .toLowerCase() blindly — use a language-aware transform (or pass the already-cased string in from the agent) so Turkish iİ and Iı are honored. JS String.toUpperCase does not apply Turkish/Azeri casing rules.
  3. 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 lack latin-ext.
  4. 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)

  1. Every identifier is imported or declared (no stray colors, interpolate…).
  2. Every @remotion/google-fonts/* import is PascalCase with NO underscores (PlayfairDisplay, not Playfair_Display) and a real installed family, else system stack; loadFont("normal", {...}) — style string first.
  3. Code is ASCII-only (smart quotes live only inside rendered strings).
  4. No top-level throws; all data baked into defaultProps; render is pure.
  5. No IIFEs in JSX — lift inline logic into a const/helper above return; every variable you compute is actually read (no no-useless-assignment).
  6. Root scene component is typed React.FC<Record<string, unknown>> and casts props inside.
  7. Font covers the content's language: matching subsets requested, language- aware casing (no blind .toUpperCase() for Turkish İ/ı), fallback to a wide-coverage family or system-ui if unsure.
  8. Transforms: only scale/translate/rotate as standalone style props — skewY, scaleX, etc. go inside ONE transform string, never duplicated.
  9. useCurrentFrame()/useVideoConfig() called only at the top of components, never module scope or plain helpers.
  10. All styling is inline style={{ ... }} — no Tailwind/className utilities (the live preview doesn't process them; the render does — they'd diverge).
  11. <Composition> width/height/fps/durationInFrames are literal integers, not constants or expressions.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.