agentsclimarketplace

Remotion video

Skill multiplex-ai/muggle-ai-teams/skills/remotion-video

AI workflow for Claude Code — describe what you want, get production-grade results. Code, content, design, planning. Built by MuggleTest.

Install
npx -y skills add multiplex-ai/muggle-ai-teams --skill remotion-video

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

  • 2 stars2 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

Generate videos programmatically using Remotion (React-based video framework). Use this skill whenever the user wants to create, render, or animate videos with code — including explainer videos, social media clips, data visualizations, motion graphics, title sequences, product demos, or any video content. Also trigger when the user mentions "remotion", "programmatic video", "video from code", "render video", "generate video", "animated video", "motion graphics", "video template", or wants to turn data/text/images into video. Even if they just say "make a video" or "create a clip", use this skill — Remotion is the tool for code-driven video generation.

SKILL.md

20.4 KB, as published. Nobody here has run it

Remotion Video Generation

You are building videos with Remotion — a React framework where videos are React components rendered frame-by-frame. Every frame is a function of the frame number and props, giving you full programmatic control.

Mental Model

A Remotion video is just a React component that receives a frame number. You render whatever you want for each frame using standard React + CSS. The framework captures each frame as an image and stitches them into a video file.

The four numbers that define every video:

  • width / height — pixel dimensions
  • fps — frames per second (typically 30)
  • durationInFrames — total frame count (seconds × fps)

Project Setup

New Project

npx create-video@latest

This scaffolds a working project. Choose a template (blank, helloworld, TailwindCSS, etc.).

Add to Existing Project

npm i remotion @remotion/cli react react-dom

Minimum Project Structure

my-video/
  public/              # Static assets (images, fonts, audio, video files)
  src/
    index.ts           # Entry point — calls registerRoot()
    Root.tsx            # Registers all compositions via <Composition>
    MyVideo.tsx         # Your video component(s)
  remotion.config.ts   # Optional CLI configuration
  package.json

Entry point (src/index.ts):

import {registerRoot} from 'remotion';
import {RemotionRoot} from './Root';
registerRoot(RemotionRoot);

Root (src/Root.tsx):

import {Composition} from 'remotion';
import {MyVideo} from './MyVideo';

export const RemotionRoot: React.FC = () => (
  <Composition
    id="my-video"
    component={MyVideo}
    durationInFrames={300}
    fps={30}
    width={1920}
    height={1080}
    defaultProps={{title: 'Hello World'}}
  />
);

Core Building Blocks

Composition — Registering a Video

Each <Composition> in Root.tsx defines a renderable video. Think of it as a video template.

<Composition
  id="my-video"           // Unique ID, used in render commands
  component={MyVideo}     // React component
  durationInFrames={300}  // 10 seconds at 30fps
  fps={30}
  width={1920}
  height={1080}
  defaultProps={{title: 'Hello'}}  // Fallback props
  schema={myZodSchema}             // Optional: enables visual editing in Studio
/>

Use <Folder name="group"> to organize compositions in the Studio sidebar.

useCurrentFrame() — The Animation Driver

Returns the current 0-indexed frame number. This is how you animate everything.

import {useCurrentFrame} from 'remotion';

const MyVideo: React.FC = () => {
  const frame = useCurrentFrame();
  return <div style={{opacity: Math.min(1, frame / 30)}}>Fading in</div>;
};

Inside a <Sequence>, frame resets to 0 relative to the sequence start.

useVideoConfig() — Video Properties

const {width, height, fps, durationInFrames} = useVideoConfig();

AbsoluteFill — The Layout Primitive

Full-size absolutely positioned container. Use it as the root of every scene and for layering content (last child renders on top).

import {AbsoluteFill} from 'remotion';

<AbsoluteFill style={{backgroundColor: 'white', justifyContent: 'center', alignItems: 'center'}}>
  <h1>Centered Title</h1>
</AbsoluteFill>

Sequence — Time-shifting Content

Controls when content appears. Children's useCurrentFrame() resets to 0 at the sequence start.

import {Sequence} from 'remotion';

// Title appears at frame 0, subtitle at frame 30
<Sequence from={0} durationInFrames={90} name="Title">
  <Title />
</Sequence>
<Sequence from={30} durationInFrames={60} name="Subtitle">
  <Subtitle />
</Sequence>
  • from — start frame (negative trims the beginning)
  • durationInFrames — how long to show (default: Infinity)
  • Children unmount outside their time range

Series — Sequential Scenes

Auto-calculates timing for back-to-back sequences:

import {Series} from 'remotion';

<Series>
  <Series.Sequence durationInFrames={60}><Intro /></Series.Sequence>
  <Series.Sequence durationInFrames={120}><MainContent /></Series.Sequence>
  <Series.Sequence durationInFrames={45}><Outro /></Series.Sequence>
</Series>

Use offset prop for overlap (negative) or gap (positive) between sequences.

Loop — Repeating Animation

import {Loop} from 'remotion';

<Loop durationInFrames={30} times={5}>
  <PulsingDot />
</Loop>

Animation

interpolate() — Map Values

The core animation function. Maps an input range to an output range.

import {interpolate, useCurrentFrame} from 'remotion';

const frame = useCurrentFrame();

// Fade in over 20 frames
const opacity = interpolate(frame, [0, 20], [0, 1], {
  extrapolateLeft: 'clamp',
  extrapolateRight: 'clamp',
});

// Slide from left
const translateX = interpolate(frame, [0, 30], [-200, 0], {
  extrapolateRight: 'clamp',
});

// Fade in AND out
const fadeInOut = interpolate(
  frame,
  [0, 20, 80, 100],
  [0, 1, 1, 0]
);

Always clamp to prevent values overshooting: {extrapolateLeft: 'clamp', extrapolateRight: 'clamp'}.

Easing — apply easing curves:

import {Easing} from 'remotion';

const value = interpolate(frame, [0, 60], [0, 1], {
  easing: Easing.bezier(0.25, 0.1, 0.25, 1),  // CSS ease equivalent
  extrapolateRight: 'clamp',
});

Available easings: Easing.linear, Easing.ease, Easing.quad, Easing.cubic, Easing.bezier(x1,y1,x2,y2), Easing.bounce, Easing.elastic(), Easing.back(), Easing.circle, Easing.sin, Easing.exp. Modifiers: Easing.in(), Easing.out(), Easing.inOut().

spring() — Physics-based Motion

Returns a value (typically 0→1) with natural spring physics. Feels more organic than easing curves.

import {spring, useCurrentFrame, useVideoConfig} from 'remotion';

const frame = useCurrentFrame();
const {fps} = useVideoConfig();

const scale = spring({frame, fps, config: {damping: 200}});
// Use: style={{transform: `scale(${scale})`}}

Parameters:

  • frame, fps — required
  • from / to — start/end values (default 0→1)
  • config.damping — deceleration (default 10; use 200 for no bounce)
  • config.mass — weight (default 1)
  • config.stiffness — bounciness (default 100)
  • config.overshootClamping — prevent overshooting
  • delay — postpone start by N frames
  • durationInFrames — stretch to exact duration
  • reverse — play backwards

Enter + exit pattern:

const enter = spring({fps, frame, config: {damping: 200}});
const exit = spring({fps, frame, delay: 60, config: {damping: 200}, reverse: true});
const scale = enter * exit;  // Or: enter - (1 - exit) depending on effect

Combining spring with interpolate to map spring output to any range:

const driver = spring({frame, fps});
const x = interpolate(driver, [0, 1], [-100, 0]);
const rotation = interpolate(driver, [0, 1], [-45, 0]);

Stagger Pattern

Offset animations for list items:

{items.map((item, i) => (
  <Sequence key={i} from={i * 8}>
    <AnimatedItem text={item} />
  </Sequence>
))}

Media

Static Assets

Place files in public/ and reference with staticFile():

import {staticFile} from 'remotion';
const logo = staticFile('logo.png');    // public/logo.png
const music = staticFile('music.mp3');  // public/music.mp3

Images

import {Img, staticFile} from 'remotion';
<Img src={staticFile('photo.png')} style={{width: 400}} />
<Img src="https://example.com/image.jpg" />

Use <Img> (not <img>) — it delays rendering until the image loads.

Video

import {OffthreadVideo, staticFile} from 'remotion';

// Preferred — uses FFmpeg for frame-accurate extraction during render
<OffthreadVideo src={staticFile('clip.mp4')} />
<OffthreadVideo src={staticFile('clip.mp4')} volume={0.5} muted />
<OffthreadVideo
  src={staticFile('clip.mp4')}
  startFrom={30}   // Skip first 30 frames
  endAt={120}       // End at frame 120
/>

For looping video, wrap in <Loop>.

Audio

import {Audio, staticFile, interpolate} from 'remotion';

<Audio src={staticFile('music.mp3')} volume={0.5} />

// Fade audio in:
<Audio
  src={staticFile('voice.mp3')}
  volume={(f) => interpolate(f, [0, 30], [0, 1], {extrapolateLeft: 'clamp', extrapolateRight: 'clamp'})}
/>

Fonts

Google Fonts (recommended):

npm i @remotion/google-fonts
import {loadFont} from '@remotion/google-fonts/Inter';
const {fontFamily} = loadFont();
// Use: style={{fontFamily}}

Local fonts:

npm i @remotion/fonts
import {loadFont} from '@remotion/fonts';
import {staticFile} from 'remotion';
loadFont({family: 'MyFont', url: staticFile('MyFont.woff2'), weight: '400'});

Transitions

npm i @remotion/transitions
import {TransitionSeries, linearTiming, springTiming} from '@remotion/transitions';
import {fade} from '@remotion/transitions/fade';
import {slide} from '@remotion/transitions/slide';
import {wipe} from '@remotion/transitions/wipe';

<TransitionSeries>
  <TransitionSeries.Sequence durationInFrames={90}>
    <SceneA />
  </TransitionSeries.Sequence>
  <TransitionSeries.Transition
    presentation={fade()}
    timing={springTiming({config: {damping: 200}})}
  />
  <TransitionSeries.Sequence durationInFrames={90}>
    <SceneB />
  </TransitionSeries.Sequence>
</TransitionSeries>

Available presentations: fade(), slide(), wipe(), and more. Timing options: linearTiming({durationInFrames}), springTiming({config}).

Data-Driven Videos

Props (Parameterized Rendering)

Pass data to videos at render time:

// Root.tsx
<Composition
  id="social-post"
  component={SocialPost}
  defaultProps={{username: 'demo', message: 'Hello!'}}
  // ...
/>
# CLI: pass JSON props
npx remotion render social-post --props='{"username":"alice","message":"Great day!"}'

Fetching Data with calculateMetadata

For async data (API calls, dynamic duration):

<Composition
  id="data-video"
  component={DataVideo}
  defaultProps={{apiUrl: 'https://api.example.com/data', data: null}}
  calculateMetadata={async ({props, abortSignal}) => {
    const res = await fetch(props.apiUrl, {signal: abortSignal});
    const data = await res.json();
    return {
      props: {...props, data},
      durationInFrames: data.items.length * 90,  // Dynamic duration
    };
  }}
  // ...
/>

Async Loading with delayRender

For assets that must load before a frame renders (fonts, images, data):

import {useCallback, useEffect, useState} from 'react';
import {continueRender, delayRender, cancelRender} from 'remotion';

const MyComp: React.FC = () => {
  const [data, setData] = useState(null);
  const [handle] = useState(() => delayRender('Fetching data...'));

  useEffect(() => {
    fetch('https://api.example.com/data')
      .then(res => res.json())
      .then(json => { setData(json); continueRender(handle); })
      .catch(err => cancelRender(err));
  }, [handle]);

  if (!data) return null;
  return <div>{data.title}</div>;
};

Always call continueRender() or cancelRender() — otherwise render hangs after 30s.

Rendering

CLI

# Render to MP4 (default)
npx remotion render my-video out/video.mp4

# Render with custom props
npx remotion render my-video --props='{"title":"Custom"}'

# Other codecs
npx remotion render my-video out/video.webm --codec=vp8
npx remotion render my-video out/video.gif --codec=gif

# Render a still image (single frame)
npx remotion still my-video out/thumbnail.png --frame=0

# Render a range of frames
npx remotion render my-video --frames=0-59

# Control quality
npx remotion render my-video --crf=18 --video-bitrate=8M

# Parallel rendering
npx remotion render my-video --concurrency=4

Key flags: --codec (h264, h265, vp8, vp9, prores, gif, mp3, aac, wav), --crf, --scale, --muted, --fps, --width, --height.

Programmatic Rendering (Node.js)

npm i @remotion/renderer @remotion/bundler
import path from 'path';
import {bundle} from '@remotion/bundler';
import {renderMedia, selectComposition} from '@remotion/renderer';

// 1. Bundle (do once, reuse for multiple renders)
const serveUrl = await bundle({
  entryPoint: path.resolve('./src/index.ts'),
});

// 2. Select composition
const composition = await selectComposition({
  serveUrl,
  id: 'my-video',
  inputProps: {title: 'Hello'},
});

// 3. Render
await renderMedia({
  composition,
  serveUrl,
  codec: 'h264',
  outputLocation: 'out/video.mp4',
  inputProps: {title: 'Hello'},
  onProgress: ({progress}) => {
    console.log(`${Math.round(progress * 100)}% done`);
  },
});

Render a still:

import {renderStill} from '@remotion/renderer';

await renderStill({
  composition,
  serveUrl,
  output: 'out/thumbnail.png',
  frame: 0,
});

Batch render from data:

for (const item of dataset) {
  const comp = await selectComposition({serveUrl, id: 'template', inputProps: item});
  await renderMedia({composition: comp, serveUrl, codec: 'h264', outputLocation: `out/${item.id}.mp4`, inputProps: item});
}

Additional Packages

Install as needed:

PackagePurpose
@remotion/transitionsScene transitions (fade, slide, wipe)
@remotion/animation-utilsCSS animation helpers (interpolateStyles, makeTransform)
@remotion/noiseSimplex noise for procedural effects
@remotion/shapesSVG shapes (<Circle>, <Rect>, <Star>, <Triangle>, <Pie>)
@remotion/pathsSVG path animation (evolvePath, interpolatePath, getLength)
@remotion/layout-utilsText measurement (measureText, fitText)
@remotion/google-fontsType-safe Google Fonts
@remotion/fontsLocal font loading
@remotion/tailwind-v4TailwindCSS v4 integration
@remotion/media-parserParse video/audio metadata without FFmpeg
@remotion/install-whisper-cppLocal audio transcription for captions
@remotion/playerEmbed Remotion preview in any React app
@remotion/threeReact Three Fiber (3D) integration
@remotion/lottieLottie animation embedding
@remotion/gifGIF embedding

TailwindCSS Setup

npm i -D @remotion/tailwind-v4 tailwindcss

In remotion.config.ts:

import {Config} from '@remotion/cli/config';
import {enableTailwind} from '@remotion/tailwind-v4';
Config.overrideWebpackConfig((config) => enableTailwind(config));

Create src/index.css:

@import 'tailwindcss';

Import in Root.tsx:

import './index.css';

Add to package.json:

{"sideEffects": ["*.css"]}

Common Patterns

Fade In/Out Text

const FadeText: React.FC<{text: string}> = ({text}) => {
  const frame = useCurrentFrame();
  const {durationInFrames} = useVideoConfig();
  const opacity = interpolate(
    frame,
    [0, 15, durationInFrames - 15, durationInFrames],
    [0, 1, 1, 0],
    {extrapolateLeft: 'clamp', extrapolateRight: 'clamp'}
  );
  return (
    <AbsoluteFill style={{justifyContent: 'center', alignItems: 'center', opacity}}>
      <h1 style={{fontSize: 80, fontWeight: 'bold'}}>{text}</h1>
    </AbsoluteFill>
  );
};

Slide-in Element

const SlideIn: React.FC<{children: React.ReactNode}> = ({children}) => {
  const frame = useCurrentFrame();
  const {fps} = useVideoConfig();
  const progress = spring({frame, fps, config: {damping: 200}});
  const x = interpolate(progress, [0, 1], [200, 0]);
  const opacity = interpolate(progress, [0, 1], [0, 1]);
  return (
    <div style={{transform: `translateX(${x}px)`, opacity}}>
      {children}
    </div>
  );
};

Animated Counter

const Counter: React.FC<{from: number; to: number}> = ({from, to}) => {
  const frame = useCurrentFrame();
  const {durationInFrames} = useVideoConfig();
  const value = interpolate(frame, [0, durationInFrames - 1], [from, to], {
    extrapolateRight: 'clamp',
  });
  return <span style={{fontSize: 120, fontWeight: 'bold'}}>{Math.round(value)}</span>;
};

Multi-scene Video

const FullVideo: React.FC = () => (
  <TransitionSeries>
    <TransitionSeries.Sequence durationInFrames={90}>
      <AbsoluteFill style={{backgroundColor: '#1a1a2e', justifyContent: 'center', alignItems: 'center'}}>
        <h1 style={{color: 'white', fontSize: 80}}>Welcome</h1>
      </AbsoluteFill>
    </TransitionSeries.Sequence>
    <TransitionSeries.Transition presentation={fade()} timing={linearTiming({durationInFrames: 20})} />
    <TransitionSeries.Sequence durationInFrames={120}>
      <MainContent />
    </TransitionSeries.Sequence>
    <TransitionSeries.Transition presentation={slide()} timing={springTiming({config: {damping: 200}})} />
    <TransitionSeries.Sequence durationInFrames={60}>
      <Outro />
    </TransitionSeries.Sequence>
  </TransitionSeries>
);

Workflow

When the user asks you to create a video:

  1. Clarify the content — What should the video show? Duration? Resolution? Format?
  2. Set up the project — Create or scaffold the Remotion project with required dependencies
  3. Build compositions — Write the React components for each scene
  4. Add animations — Use interpolate(), spring(), and <Sequence> for motion
  5. Handle media — Import images, video, audio, fonts as needed
  6. Preview — Run npx remotion studio for the visual preview
  7. Render — Use CLI or programmatic rendering to output the final video

For more details on any API, read the reference files in references/ or consult the Remotion docs.

Lessons Learned (from production use)

objectFit: contain breaks coordinate mapping

When using objectFit: 'contain' on <Img>, images with different aspect ratios than the canvas get margins. Any overlay positioning (cursors, text, click effects) must account for the contain layout.

// Compute contain margins before mapping coordinates
function getContainLayout(imageWidth: number, imageHeight: number) {
  const imageAspect = imageWidth / imageHeight;
  const canvasAspect = 1920 / 1080;
  if (imageAspect > canvasAspect) {
    // Wider: fit width, bars top/bottom
    const displayHeight = 1920 / imageAspect;
    return { offsetX: 0, offsetY: (1080 - displayHeight) / 2, displayWidth: 1920, displayHeight };
  } else {
    // Taller: fit height, bars left/right
    const displayWidth = 1080 * imageAspect;
    return { offsetX: (1920 - displayWidth) / 2, offsetY: 0, displayWidth, displayHeight: 1080 };
  }
}

Always pass source image {width, height} to coordinate mapping functions. A 28px offset is clearly visible at 1080p.

Reusable scenes must use proportional timing for ALL parameters

When a scene component is reused across compositions with different durationInFrames, every animation parameter must scale — not just keyframe positions.

Bad: charsPerSecond={6} — typing gets cut off in shorter compositions. Good: charsPerSecond = textLength / (availableRatio * durationInFrames / fps)

Use const t = (ratio: number) => Math.round(ratio * durationInFrames) for keyframes, AND calculate rates dynamically for TypeWriter, scroll speed, etc.

Self-check rendered output at full scale

Never declare a scene "passes" from small cropped images. Always:

  1. Render stills at key frames via npx remotion still
  2. Serve via python3 -m http.server
  3. Open in Playwright browser at full viewport size
  4. Compare cursor/text positions against original screenshot elements
  5. Use before/after diff (stack vertically) to verify alignment

Editing text out of JPEG screenshots

Don't paint solid rectangles — JPEG compression artifacts make them visible. Instead, clone pixels per-column from an adjacent empty region:

# Clone from row above text, preserving JPEG block patterns per column
for x in range(text_x_start, text_x_end):
    source_pixel = img.getpixel((x, y_above_text))
    for y in range(text_y_start, text_y_end):
        img.putpixel((x, y), source_pixel)

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.