
Remotion
- 421 installs
- 42.8k repo stars
- Updated July 24, 2026
- calesthio/openmontage
Use this skill when working with remotion.
About
Skill for working with remotion. Use when you need remotion functionality in your application.
- Specialized for remotion
- Integrated with Claude Code
- Streamlines workflow
Remotion by the numbers
- 421 all-time installs (skills.sh)
- Ranked #648 of 2,277 Frontend Development skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/calesthio/openmontage --skill remotionAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 421 |
|---|---|
| repo stars | ★ 42.8k |
| Last updated | July 24, 2026 |
| Repository | calesthio/openmontage ↗ |
What it does
Use this skill when working with remotion.
Files
Remotion — Toolkit Extensions
Core Remotion knowledge lives in .claude/skills/remotion-official/ (synced from the official remotion-dev/skills repo). This file covers toolkit-specific patterns only.Shared Components
Reusable video components in lib/components/. Import in templates via:
import { AnimatedBackground, SlideTransition, Label } from '../../../../lib/components';| Component | Purpose |
|---|---|
AnimatedBackground | Floating shapes background (variants: subtle, tech, warm, dark) |
SlideTransition | Scene transitions (fade, zoom, slide-up, blur-fade) |
Label | Floating label badge with optional JIRA reference |
Vignette | Cinematic edge darkening overlay |
LogoWatermark | Corner logo branding |
SplitScreen | Side-by-side video comparison |
NarratorPiP | Picture-in-picture presenter overlay |
Envelope | 3D envelope with opening flap animation |
PointingHand | Animated hand emoji with slide-in and pulse |
MazeDecoration | Animated isometric grid decoration for corners |
Custom Transitions
The toolkit includes a transitions library at lib/transitions/ for scene-to-scene effects beyond the official @remotion/transitions package.
Using TransitionSeries
import { TransitionSeries, linearTiming } from '@remotion/transitions';
// Import custom transitions from lib (adjust path based on your project location)
import { glitch, lightLeak, clockWipe, checkerboard } from '../../../../lib/transitions';
// Or import from @remotion/transitions for official ones
import { slide, fade } from '@remotion/transitions/slide';
<TransitionSeries>
<TransitionSeries.Sequence durationInFrames={90}>
<TitleSlide />
</TransitionSeries.Sequence>
<TransitionSeries.Transition
presentation={glitch({ intensity: 0.8 })}
timing={linearTiming({ durationInFrames: 30 })}
/>
<TransitionSeries.Sequence durationInFrames={120}>
<ContentSlide />
</TransitionSeries.Sequence>
</TransitionSeries>Available Custom Transitions
| Transition | Options | Best For |
|---|---|---|
glitch() | intensity, slices, rgbShift | Tech demos, edgy reveals, cyberpunk |
rgbSplit() | direction, displacement | Modern tech, energetic transitions |
zoomBlur() | direction, blurAmount | CTAs, high-energy moments, impact |
lightLeak() | temperature, direction | Celebrations, film aesthetic, warm moments |
clockWipe() | startAngle, direction, segments | Time-related content, playful reveals |
pixelate() | maxBlockSize, gridSize, scanlines, glitchArtifacts, randomness | Retro/gaming, digital transformations |
checkerboard() | gridSize, pattern, stagger, squareAnimation | Playful reveals, structured transitions |
Checkerboard patterns: sequential, random, diagonal, alternating, spiral, rows, columns, center-out, corners-in
Transition Examples
// Tech/cyberpunk feel
glitch({ intensity: 0.8, slices: 8, rgbShift: true })
// Warm celebration
lightLeak({ temperature: 'warm', direction: 'right' })
// High energy zoom
zoomBlur({ direction: 'in', blurAmount: 20 })
// Chromatic aberration
rgbSplit({ direction: 'diagonal', displacement: 30 })
// Clock sweep reveal
clockWipe({ direction: 'clockwise', startAngle: 0 })
// Retro pixelation
pixelate({ maxBlockSize: 50, glitchArtifacts: true })
// Checkerboard patterns
checkerboard({ pattern: 'diagonal', gridSize: 8 })
checkerboard({ pattern: 'spiral', gridSize: 10 })
checkerboard({ pattern: 'center-out', squareAnimation: 'scale' })Transition Duration Guidelines
| Type | Frames | Notes |
|---|---|---|
| Quick cut | 15-20 | Fast, punchy |
| Standard | 30-45 | Most common |
| Dramatic | 50-60 | Slow reveals |
| Glitch effects | 20-30 | Should feel sudden |
| Light leak | 45-60 | Needs time to sweep |
Preview Transitions
Run the showcase gallery to see all transitions:
cd showcase/transitions && npm run studioToolkit Best Practices
1. Frame-based animations only — Avoid CSS transitions/animations; they cause flickering during render 2. Use fps from useVideoConfig() — Make animations frame-rate independent 3. Clamp interpolations — Use extrapolateRight: 'clamp' to prevent runaway values 4. Use OffthreadVideo — Better performance than <Video> for complex compositions 5. delayRender for async — Always block rendering until data is ready 6. staticFile for assets — Reference files from public/ folder correctly 7. All projects use 30fps — Timing: frames = seconds × 30 8. playbackRate must be constant — For variable/extreme speeds, pre-process with FFmpeg
Project Timing Conventions
| Scene Type | Duration | Notes |
|---|---|---|
| Title | 3-5s (90-150f) | Logo + headline |
| Overview | 10-20s | 3-5 bullet points |
| Demo | 10-30s | Adjust playbackRate to fit |
| Stats | 8-12s | 3-4 stat cards |
| Credits | 5-10s | Quick fade |
Pacing: ~150 words/minute for voiceover. Voiceover drives timing.
Advanced API
For detailed API documentation on all hooks, components, renderer, Lambda, and Player APIs, see reference.md.
License Note
Remotion has a special license. Companies may need to obtain a license for commercial use. Check https://remotion.dev/license
---
Feedback & Contributions
If this skill is missing information or could be improved:
- Missing a pattern? Describe what you needed
- Found an error? Let me know what's wrong
- Want to contribute? I can help you:
1. Update this skill with improvements 2. Create a PR to github.com/digitalsamba/claude-code-video-toolkit
Just say "improve this skill" and I'll guide you through updating .claude/skills/remotion/SKILL.md.
Remotion API Reference
Core Hooks
useCurrentFrame()
const frame = useCurrentFrame();Returns current frame (0-indexed). Inside <Sequence>, returns relative frame.
useVideoConfig()
const { width, height, fps, durationInFrames, id, defaultProps } = useVideoConfig();interpolate()
interpolate(
input: number,
inputRange: number[],
outputRange: number[],
options?: {
extrapolateLeft?: 'extend' | 'clamp' | 'identity' | 'wrap',
extrapolateRight?: 'extend' | 'clamp' | 'identity' | 'wrap',
easing?: (t: number) => number
}
): numberExamples:
// Basic interpolation
interpolate(15, [0, 30], [0, 100]); // 50
// With clamping
interpolate(50, [0, 30], [0, 1], { extrapolateRight: 'clamp' }); // 1
// Multiple keyframes
interpolate(frame, [0, 20, 40, 60], [0, 1, 1, 0]);
// With easing
interpolate(frame, [0, 30], [0, 100], { easing: Easing.bezier(0.42, 0, 0.58, 1) });spring()
spring({
frame: number,
fps: number,
config?: {
damping?: number, // Default: 10
mass?: number, // Default: 1
stiffness?: number, // Default: 100
overshootClamping?: boolean
},
from?: number, // Default: 0
to?: number, // Default: 1
durationInFrames?: number,
durationRestThreshold?: number,
delay?: number,
reverse?: boolean
}): numberConfig presets:
- High bounce:
{ damping: 5, stiffness: 200 } - No bounce:
{ damping: 20, stiffness: 100, overshootClamping: true } - Slow:
{ damping: 20, mass: 2 }
measureSpring()
Get the duration of a spring animation:
import { measureSpring } from 'remotion';
const duration = measureSpring({ fps: 30, config: { damping: 10 } });
// Returns number of frames until spring settlesinterpolateColors()
interpolateColors(
input: number,
inputRange: number[],
outputRange: string[], // Hex, rgb(), rgba(), hsl()
options?: { extrapolateLeft?, extrapolateRight? }
): stringEasing
import { Easing } from 'remotion';
// Basic
Easing.linear
Easing.ease
Easing.quad
Easing.cubic
// In/Out/InOut variants
Easing.in(Easing.quad)
Easing.out(Easing.cubic)
Easing.inOut(Easing.ease)
// Cubic bezier
Easing.bezier(x1, y1, x2, y2)
// Other
Easing.circle
Easing.back(s?) // Overshoot
Easing.elastic(bounciness?)
Easing.bounce
Easing.sin
Easing.exp
Easing.poly(n) // Power of nComponents
Composition
<Composition
id="MyVideo"
component={MyComponent}
// OR lazyComponent={() => import('./MyComponent')}
durationInFrames={150}
fps={30}
width={1920}
height={1080}
defaultProps={{ title: 'Hello' }}
calculateMetadata={async ({ props }) => ({
durationInFrames: props.items.length * 30,
props: { ...props, computed: true }
})}
/>Sequence
<Sequence
from={30} // Start frame
durationInFrames={60} // Optional duration
name="Intro" // Label in Studio timeline
layout="none" // "none" | "absolute-fill"
>
<Child />
</Sequence>Series
<Series>
<Series.Sequence durationInFrames={30} offset={-5}>
<A /> {/* Frames 0-29 */}
</Series.Sequence>
<Series.Sequence durationInFrames={60}>
<B /> {/* Frames 25-84 (offset caused overlap) */}
</Series.Sequence>
</Series>Loop
<Loop
durationInFrames={30}
times={3} // Or Infinity
layout="none"
>
<Animation />
</Loop>AbsoluteFill
<AbsoluteFill style={{ backgroundColor: '#000' }}>
{/* Position: absolute, full width/height */}
</AbsoluteFill>Media Components
Img (waits for load):
<Img src={staticFile('photo.jpg')} style={{ width: '100%' }} />Video/Html5Video:
<Video
src={staticFile('clip.mp4')}
volume={0.5} // 0-1, or callback: (f) => f / 100
playbackRate={1.5}
muted={false}
loop={false}
startFrom={30} // Skip first 30 frames of source
endAt={120} // Stop at frame 120 of source
acceptableTimeShiftInSeconds={0.2}
/>OffthreadVideo (better performance):
<OffthreadVideo
src={staticFile('clip.mp4')}
volume={0.5}
transparent={false} // For videos with alpha
toneMapped={true} // HDR tone mapping
/>Audio:
<Audio
src={staticFile('music.mp3')}
volume={0.8}
startFrom={0}
endAt={300}
playbackRate={1}
/>AnimatedImage (GIF/APNG):
<AnimatedImage src={staticFile('animation.gif')} />Async Handling
import { delayRender, continueRender, cancelRender } from 'remotion';
// Block render
const handle = delayRender('Loading data...');
// Unblock when ready
continueRender(handle);
// Cancel on error
cancelRender(new Error('Failed to load'));With timeout:
const handle = delayRender('Loading...', { timeoutInMilliseconds: 30000 });Static Files & Prefetching
import { staticFile, prefetch, getStaticFiles } from 'remotion';
// Reference file in public/
const url = staticFile('video.mp4');
// Prefetch for faster playback
const { free, waitUntilDone } = prefetch(url);
await waitUntilDone();
// Later: free() to release memory
// List all static files
const files = getStaticFiles(); // ['video.mp4', 'image.png', ...]Input Props
import { getInputProps } from 'remotion';
const props = getInputProps(); // Data passed via --props CLI flagEnvironment Detection
import { getRemotionEnvironment } from 'remotion';
const env = getRemotionEnvironment();
// { isStudio: boolean, isRendering: boolean, isPlayer: boolean }random()
Deterministic random for consistent renders:
import { random } from 'remotion';
const value = random('my-seed'); // 0-1, same every render
const value2 = random('seed', 0, 100); // 0-100
const value3 = random(null); // Different each render@remotion/renderer API
import { bundle } from '@remotion/bundler';
import {
renderMedia,
renderStill,
selectComposition,
getCompositions,
renderFrames,
stitchFramesToVideo
} from '@remotion/renderer';
// Bundle project
const bundleLocation = await bundle({
entryPoint: './src/index.ts',
webpackOverride: (config) => config,
});
// Get composition
const composition = await selectComposition({
serveUrl: bundleLocation,
id: 'MyComp',
inputProps: {},
});
// Render video
await renderMedia({
composition,
serveUrl: bundleLocation,
codec: 'h264', // h264, h265, vp8, vp9, gif, prores
outputLocation: 'out.mp4',
inputProps: {},
onProgress: ({ progress }) => console.log(`${progress * 100}%`),
imageFormat: 'jpeg', // jpeg or png
jpegQuality: 80,
scale: 1,
frameRange: [0, 59], // Optional: specific frames
muted: false,
audioBitrate: '128k',
videoBitrate: '5M',
crf: 18, // Quality (lower = better, bigger)
concurrency: 4,
});
// Render still image
await renderStill({
composition,
serveUrl: bundleLocation,
output: 'thumbnail.png',
frame: 30,
imageFormat: 'png',
});@remotion/lambda API
import {
deployFunction,
deploySite,
renderMediaOnLambda,
renderStillOnLambda,
getRenderProgress,
downloadMedia,
} from '@remotion/lambda';
// Deploy function
const { functionName } = await deployFunction({
region: 'us-east-1',
timeoutInSeconds: 120,
memorySizeInMb: 2048,
});
// Deploy site
const { serveUrl } = await deploySite({
entryPoint: './src/index.ts',
region: 'us-east-1',
siteName: 'my-video',
});
// Render
const { renderId, bucketName } = await renderMediaOnLambda({
region: 'us-east-1',
functionName,
serveUrl,
composition: 'MyComp',
codec: 'h264',
inputProps: {},
framesPerLambda: 20,
});
// Check progress
const progress = await getRenderProgress({
renderId,
bucketName,
region: 'us-east-1',
functionName,
});
// Download when done
if (progress.done) {
await downloadMedia({
bucketName,
renderId,
region: 'us-east-1',
outPath: 'video.mp4',
});
}@remotion/player API
import { Player, PlayerRef } from '@remotion/player';
const playerRef = useRef<PlayerRef>(null);
<Player
ref={playerRef}
component={MyComp}
// OR lazyComponent={() => import('./MyComp')}
durationInFrames={150}
fps={30}
compositionWidth={1920}
compositionHeight={1080}
inputProps={{}}
style={{ width: '100%' }}
controls={true}
autoPlay={false}
loop={false}
showVolumeControls={true}
allowFullscreen={true}
clickToPlay={true}
doubleClickToFullscreen={true}
spaceKeyToPlayOrPause={true}
playbackRate={1}
renderLoading={() => <div>Loading...</div>}
errorFallback={({ error }) => <div>Error: {error.message}</div>}
numberOfSharedAudioTags={5}
initiallyShowControls={3000}
renderPlayPauseButton={() => null}
moveToBeginningWhenEnded={true}
/>
// Imperative API
playerRef.current?.play();
playerRef.current?.pause();
playerRef.current?.toggle();
playerRef.current?.seekTo(30);
playerRef.current?.getCurrentFrame();
playerRef.current?.isPlaying();
playerRef.current?.getVolume();
playerRef.current?.setVolume(0.5);
playerRef.current?.isMuted();
playerRef.current?.mute();
playerRef.current?.unmute();
playerRef.current?.requestFullscreen();
playerRef.current?.exitFullscreen();
playerRef.current?.isFullscreen();
// Events
<Player
onPlay={() => {}}
onPause={() => {}}
onEnded={() => {}}
onError={(e) => {}}
onSeeked={(frame) => {}}
onTimeUpdate={({ frame }) => {}}
onFullscreenChange={(isFullscreen) => {}}
/>calculateMetadata
Dynamic composition properties:
export const calculateMetadata: CalculateMetadataFunction<Props> = async ({
props,
abortSignal,
defaultProps
}) => {
const data = await fetch('/api/data', { signal: abortSignal });
return {
durationInFrames: data.items.length * 30,
fps: 60,
width: 1920,
height: 1080,
props: { ...props, items: data.items },
};
};
<Composition
id="Dynamic"
component={MyComp}
calculateMetadata={calculateMetadata}
// Base values (can be overridden by calculateMetadata)
durationInFrames={1}
fps={30}
width={1920}
height={1080}
/>