Generate production-ready 2D TSX video files for VidTSX (Remotion-based) from a shot, scene, or video description. Use whenever the user wants to create or generate a VidTSX video, a 2D TSX shot or scene, an animated clip, title card, or rendered motion graphic — including when they describe a visual animation they want built, or say "make a shot", "build this scene", "generate the TSX", or "turn this into a video". Covers the mandatory file structure and composition config, dimension presets (horizontal/vertical/square), six style presets (minimalist, memphis, neo-brutalism, glassmorphism, neon, corporate), and the hard rules that keep renders from crashing, covering frame-based animation only (no useState/useEffect/setTimeout), strictly monotonic interpolate ranges, Easing.bezier not wrapper syntax, and the chroma-js and @remotion/paths import gotchas. Not for 3D/three.js compositions, general React work, or editing video files.
npx skills add https://github.com/hassancs91/claude-youtube-editor --skill vidtsx-2d-generator
Generate production-ready 2D TSX video files for VidTSX from a description of a shot or video. The files are Remotion compositions that VidTSX renders frame-by-frame, so correctness matters more than cleverness: a single non-monotonic interpolate range or a stray useState will crash the render.
This skill covers 2D motion graphics. 3D work (@remotion/three / React Three Fiber) is out of scope.
.tsx file directly..tsx file, not inline code. These TSX components always exceed 20 lines, and the user edits these files directly. Default to one shot per file (atomic, easy to edit) unless the user asks for a single combined deliverable.| Format | Width | Height | Use case |
|------------|-------|--------|----------------------------|
| horizontal | 1920 | 1080 | YouTube, presentations |
| vertical | 1080 | 1920 | TikTok, Reels, Shorts |
| square | 1080 | 1080 | Instagram feed |
references/style-presets.md)Every generated file follows this skeleton. The labeled section comments are intentional — they keep large files navigable.
import React from 'react';
import {
useCurrentFrame,
useVideoConfig,
interpolate,
Easing,
AbsoluteFill,
Sequence,
} from 'remotion';
// =============================================================================
// COMPOSITION CONFIG
// =============================================================================
export const compositionConfig = {
id: 'ComponentName', // PascalCase only — NO hyphens or underscores
durationInSeconds: 5,
fps: 30,
width: 1920,
height: 1080,
};
// =============================================================================
// STYLE CONSTANTS
// =============================================================================
const COLORS = {
primary: '#6366f1',
secondary: '#8b5cf6',
accent: '#06b6d4',
background: '#0f0f23',
text: '#ffffff',
} as const;
const TYPOGRAPHY = {
fontFamily: 'Inter, system-ui, sans-serif',
} as const;
const EASINGS = {
easeOut: Easing.bezier(0.33, 1, 0.68, 1),
easeIn: Easing.bezier(0.32, 0, 0.67, 0),
easeInOut: Easing.bezier(0.37, 0, 0.63, 1),
overshoot: Easing.bezier(0.34, 1.56, 0.64, 1),
} as const;
// =============================================================================
// PRE-GENERATED DATA (computed once at module level, NOT during render)
// =============================================================================
const seededRandom = (seed: number): number => {
const x = Math.sin(seed * 9999) * 10000;
return x - Math.floor(x);
};
// =============================================================================
// MAIN COMPONENT
// =============================================================================
const ComponentName: React.FC = () => {
const frame = useCurrentFrame();
const { fps, durationInFrames, width, height } = useVideoConfig();
return (
<AbsoluteFill style={{ backgroundColor: COLORS.background }}>
{/* Content */}
</AbsoluteFill>
);
};
export default ComponentName;
Swap ComponentName for a PascalCase name describing the shot, set the COLORS block from the chosen style preset, and set width/height/durationInSeconds/fps from the request.
These are not stylistic preferences — they are what makes a render succeed. VidTSX (via Remotion) renders each frame by calling the component at a fixed frame number. There is no event loop, no persistence between frames, and no wall-clock time. Anything that assumes those things will break.
useCurrentFrame() + interpolate().useState, useEffect, setTimeout, setInterval, or CSS animations/transitions. They don't fit the frame-by-frame model and produce broken or non-deterministic output.seededRandom (or an equivalent seeded function) so every render of a given frame is identical. Pre-generate particle/random arrays at module level, never inside the render function — recomputing per frame causes flicker.interpolate's input range has to be strictly monotonically increasing. Duplicate or descending values throw at runtime.
// ✅ Correct
interpolate(frame, [0, 30, 60], [0, 1, 0]);
// ❌ Throws — input range descends
interpolate(frame, [60, 30, 0], [0, 1, 0]);
To reverse a mapping, flip the *output* range, never the input range:
// ✅ Correct — maps 0→100, 1→0
interpolate(value, [0, 1], [100, 0]);
// ❌ Wrong
interpolate(value, [1, 0], [100, 0]);
For index-based timing, make sure startFrame < endFrame and always clamp:
const startFrame = index * 30;
const endFrame = startFrame + 30;
interpolate(frame, [startFrame, endFrame], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
Always pass extrapolateLeft: 'clamp' and extrapolateRight: 'clamp' unless an unbounded value is genuinely wanted — without them, values shoot past their range before/after the keyframes.
Wrapper forms like Easing.out(Easing.cubic) crash. Define named beziers once (see the EASINGS block in the skeleton) and reference them:
// ❌ Crashes
Easing.out(Easing.cubic);
Easing.in(Easing.quad);
// ✅ Correct
interpolate(frame, [0, 30], [0, 1], {
easing: EASINGS.easeOut,
extrapolateRight: 'clamp',
});
// ✅ Correct
import * as chroma from 'chroma-js';
const color = chroma('#00bfff').brighten(0.5).hex();
// ❌ Wrong — "chroma is not a function"
import chroma from 'chroma-js';
These helpers do not exist and must never be used:
makeCircle(), makeRect(), makeTriangle(), makeLine(), makePie(), makePolygon(), makeEllipse(), makeStar().
Only these imports are valid:
import { evolvePath, getLength, getPointAtLength, getTangentAtLength } from '@remotion/paths';
Write SVG path strings by hand and animate them with evolvePath:
const circlePath = 'M 50 10 A 40 40 0 1 1 49.99 10 Z';
const rectPath = 'M 0 0 L 100 0 L 100 50 L 0 50 Z';
const linePath = 'M 0 0 L 100 100';
const { strokeDasharray, strokeDashoffset } = evolvePath(progress, rectPath);
compositionConfig.id is PascalCase only — no hyphens, no underscores (e.g. ProductReveal, not product-reveal or product_reveal).
const centered: React.CSSProperties = {
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
};
| Element | Size | Weight |
|---------------|------------|---------|
| Headlines | 72–120px | 700–900 |
| Subheadlines | 36–48px | 500–700 |
| Body | 28–36px | 400–500 |
Always set margin: 0 on text elements — browser defaults push layouts off-center.
When the user names a style, load its palette and characteristics from references/style-presets.md and drop the COLORS object into the skeleton. The six presets are: minimalist (default), memphis, neo-brutalism, glassmorphism, neon/cyberpunk, and corporate. If no style is named, use minimalist.
A shot is not done until you have looked at it. After writing the .tsx, render at least one still and open it — never hand over a shot you have only reasoned about. Rendering succeeds and the numbers look right, yet text overflows its card, an image is cropped at the wrong crop, an element is off-screen, or two things overlap. Only a screenshot catches these.
node scripts/render-all.mjs --still --scale=1 <ShotId>) and Read the PNG.ffmpeg -ss <t> -i out/<id>.mp4 -frames:v 1 f.jpg), or render stills at several offsets. Verify the state at each cue, not just one frame.objectFit: contain on a matching background for heterogeneous real images), text is not clipped or overflowing, nothing is off the safe area, and each animated element is actually visible when it should be.Fix what the screenshot reveals, then re-render and look again. Treat "I rendered it and it looks correct" — with the frame shown — as the bar for done.
Generate the complete .tsx file and nothing else inside it — no surrounding markdown, no commentary before or after the code in the file itself. A short one-line note in chat when handing over the file is fine.
Take hassancs91/vidtsx-2d-generator from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.