How I Built a Programmatic Video Engine for SaaS Product Demos
Code-Driven Video Rendering: Building Resilient SaaS Demos with React and Remotion
Current Situation Analysis
Modern SaaS products evolve rapidly. Features ship weekly, UIs undergo redesigns, and navigation structures shift based on user feedback. Marketing assets, however, remain stubbornly static. The industry standard for product demos relies on screen recordings or outsourced motion design, both of which create brittle artifacts that decay the moment code is merged.
Screen recordings capture pixels, not logic. When a button moves or a modal changes behavior, the video becomes misleading. Re-recording introduces latency, version drift, and quality degradation. Motion design offers higher fidelity but introduces a dependency loop: engineering changes the product, marketing waits weeks for a designer to update the asset, and the cycle repeats. This friction causes teams to ship outdated demos or skip video entirely, directly impacting conversion rates.
The solution lies in treating video as code. By leveraging React-based rendering engines like Remotion, teams can generate cinematic walkthroughs programmatically. This approach captures intent rather than pixels. When the UI changes, the component updates, and the video re-renders with the new state while preserving choreography, camera movement, and interactions.
Evidence of this shift is visible in the maturity of the tooling. Modern programmatic video frameworks now support complex production requirements: 500+ unit tests for engine stability, integration with React 19 and TypeScript 5.9, schema validation via Zod, and output capabilities up to 1920x1080 at 30fps. The technology has moved from experimental to production-ready, enabling workflows where video assets are version-controlled, branched, and code-reviewed alongside the product itself.
WOW Moment: Key Findings
The transition from static recording to programmatic rendering fundamentally alters the economics and agility of video production. The following comparison highlights the operational differences between traditional methods and a code-driven approach.
| Approach | Update Latency | Version Control | UI Drift Risk | Iteration Cost | Collaboration Model |
|---|---|---|---|---|---|
| Screen Recording | Days | None | High | Low (Internal) | Linear |
| Motion Design | Weeks | File-based | Medium | High | Handoff |
| Programmatic | Minutes | Git-native | Zero | Near Zero | Concurrent |
Why this matters:
Programmatic video enables git diff on marketing assets. A developer can update a component, and the video automatically reflects the change. Non-technical stakeholders can adjust timing, copy, and layout via visual property editors without touching source code. This collapses the feedback loop from "file a request and wait" to "modify and preview," allowing marketing and engineering to iterate in parallel.
Core Solution
Building a resilient programmatic video engine requires a layered architecture that separates motion logic, visual primitives, and editor tooling. The goal is a system where the composition is pure, props drive all visual state, and interactions are synchronized via a timeline.
Architecture Overview
- Engine Layer: Handles low-level motion. This includes the choreography resolver (calculating element positions per frame), the cursor system (interpolation, anchoring, shape switching), the camera rig (global transforms), and audio management (ducking, fading).
- Primitives Layer: Reusable visual components. Examples include macOS-style windows, animated headlines, end cards, and a library of app-UI blocks (sidebars, data tables, stat cards) that compose into realistic interfaces.
- Editor Layer: A separate overlay that reads and writes input props. It provides drag-to-move, resize handles, snap guides, and inline text editing. Crucially, the editor communicates with the composition via a
postMessagebridge, ensuring the composition remains pure and can be rendered without the editor overhead. - Interaction Layer: A
UIKeyframesystem that defines state changes on a timeline. This syncs cursor actions with UI responses. When the cursor clicks a button, the keyframe triggers the button's active state automatically.
Implementation Details
The following TypeScript examples demonstrate a prop-driven scene definition, a geometry-aware cursor controller, and a schema-validated layout system.
1. Scene Definition with Prop-Driven Choreography
Scenes should accept all visual configuration via props. This allows the editor to manipulate layout without modifying logic.
import { useVideoConfig, interpolate } from 'remotion';
import { SceneProps } from './types';
export const ProductShowcase: React.FC<SceneProps> = ({
layout,
cursor,
camera,
timeline,
}) => {
const { width, height, fps } = useVideoConfig();
// Camera transforms based on scene-relative keyframes
const cameraScale = interpolate(
timeline.currentFrame,
[camera.keyframes[0].frame, camera.keyframes[1].frame],
[camera.keyframes[0].scale, camera.keyframes[1].scale],
{ extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }
);
const cameraTranslateX = interpolate(
timeline.currentFrame,
[camera.keyframes[0].frame, camera.keyframes[1].frame],
[camera.keyframes[0].x, camera.keyframes[1].x],
{ extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }
);
return (
<div style={{ transform: `scale(${cameraScale}) translateX(${cameraTranslateX}px)` }}>
<Window
id={layout.windowId}
position={layout.position}
size={layout.size}
zIndex={layout.zIndex}
title={layout.title}
>
<DashboardUI
sidebarActive={timeline.sidebarActiveItem}
tableData={timeline.tableData}
/>
</Window>
<CursorController
waypoints={cursor.waypoints}
currentFrame={timeline.currentFrame}
activeElementId={cursor.activeElementId}
/>
</div>
);
};
2. Geometry-Aware Cursor Controller
The cursor must target real elements and follow smooth paths. It should change shape based on interaction context.
import { interpolate, spring } from 'remotion';
import { CursorWaypoint, CursorStyle } from './types';
export const CursorController: React.FC<{
waypoints: CursorWaypoint[];
currentFrame: number;
activeElementId: string | null;
}> = ({ waypoints, currentFrame, activeElementId }) => {
// Interpolate position between waypoints
const segmentIndex = waypoints.findIndex(
(wp, i) => currentFrame >= wp.frame && currentFrame < (waypoints[i + 1]?.frame ?? Infinity)
);
const currentWaypoint = waypoints[segmentIndex];
const nextWaypoint = waypoints[segmentIndex + 1];
if (!currentWaypoint || !nextWaypoint) return null;
const progress = (currentFrame - currentWaypoint.frame) / (nextWaypoint.frame - currentWaypoint.frame);
// Apply easing for natural movement
const easedProgress = interpolate(progress, [0, 1], [0, 1], { easing: 'easeInOut' });
const x = interpolate(easedProgress, [0, 1], [currentWaypoint.x, nextWaypoint.x]);
const y = interpolate(easedProgress, [0, 1], [currentWaypoint.y, nextWaypoint.y]);
// Determine cursor style based on active element
const style: CursorStyle = activeElementId === 'submit-btn' ? 'pointer' : 'default';
return (
<div
style={{
position: 'absolute',
left: x,
top: y,
cursor: style,
transition: 'none', // Remotion handles frame updates
}}
>
<CursorIcon type={style} />
</div>
);
};
3. Schema-Validated Layout System
Use Zod to validate JSON descriptors for app UI. This ensures that imported designs or programmatic layouts conform to expected structures.
import { z } from 'zod';
export const LayoutSchema = z.object({
windowId: z.string().uuid(),
position: z.object({ x: z.number(), y: z.number() }),
size: z.object({ width: z.number().min(200), height: z.number().min(150) }),
zIndex: z.number().int().min(0),
title: z.string().max(50),
children: z.array(z.string()).optional(),
});
export type LayoutProps = z.infer<typeof LayoutSchema>;
// Usage in composition
const safeLayout = LayoutSchema.parse(rawLayoutData);
Architecture Rationale:
- Prop-Driven Design: Decouples visual configuration from rendering logic. Enables visual editors to modify props without altering code.
- Scene-Relative Timing: Camera and cursor keyframes reference scene names or relative offsets, not absolute frame numbers. This prevents breakage when scene durations change.
- Schema Validation: Zod ensures data integrity for JSON descriptors, catching errors early during development or import.
- Editor Separation: The editor overlay uses
postMessageto communicate with the composition. This keeps the production render lightweight and free of editor artifacts.
Pitfall Guide
Building a programmatic video engine introduces unique challenges. The following pitfalls are common in production environments.
Absolute Frame Dependencies
- Explanation: Hardcoding frame numbers for cursor waypoints or camera keyframes causes cascading failures when scene durations change.
- Fix: Use scene-relative timing. Reference scene names or relative offsets. Implement a timeline resolver that calculates absolute frames based on scene durations.
Editor-Composition Coupling
- Explanation: Embedding editor logic directly in the composition increases bundle size and complicates rendering.
- Fix: Maintain a strict separation. The composition should be pure. Use a
postMessagebridge for the editor overlay to read/write props. Disable the editor layer during production renders.
Cursor Jitter and Unnatural Movement
- Explanation: Linear interpolation between waypoints results in robotic cursor movement.
- Fix: Apply easing functions (e.g.,
easeInOut) to cursor paths. Use bezier curves for complex trajectories. Ensure the cursor speed varies realistically based on distance.
Schema Drift in UI Descriptors
- Explanation: JSON descriptors for app UI can drift from the expected structure, causing runtime errors or broken layouts.
- Fix: Enforce strict schema validation using Zod. Validate all imported data and programmatic descriptors before rendering. Provide clear error messages for schema violations.
Audio Clipping and Ducking Issues
- Explanation: Background music and sound effects can clash, causing audio distortion or masking important cues.
- Fix: Implement volume ducking logic. Automatically lower music volume when sound effects play. Use audio managers to handle fading and synchronization with video frames.
Ignoring Text Bounds and Alignment
- Explanation: Inline text editing in the editor can result in text overflowing containers or misaligning with UI elements.
- Fix: Enforce text bounds in the editor. Provide snap guides for alignment. Validate text length against container size during schema validation.
Performance Bottlenecks in Complex Scenes
- Explanation: Rendering high-fidelity UI with many animated elements can cause frame drops or slow render times.
- Fix: Optimize component re-renders. Use
React.memofor static elements. Profile render performance and simplify complex animations where possible.
Production Bundle
Action Checklist
- Define Schema: Create Zod schemas for layout, cursor, and timeline data to ensure validation.
- Build Primitives: Develop reusable components for windows, headlines, and app-UI blocks.
- Wire Cursor: Implement the cursor controller with easing, shape switching, and element anchoring.
- Configure Camera: Set up scene-relative camera keyframes with zoom, pan, and rotation.
- Add Audio: Integrate audio manager with ducking and fading logic.
- Test Render: Run end-to-end tests to verify synchronization and output quality.
- Document API: Provide clear documentation for scene creation and customization.
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|---|---|---|
| Rapid UI Changes | Programmatic Video | Updates are instant; version-controlled; zero UI drift. | Low (Engineering time) |
| High-Fidelity Branding | Motion Design | Superior artistic control; custom animations. | High (Designer fees) |
| Quick Internal Demo | Screen Recording | Fastest setup; no code required. | Low (Internal time) |
| Scalable Marketing | Programmatic Video | Reusable templates; prop-driven customization; Git workflow. | Medium (Initial setup) |
Configuration Template
remotion.config.ts
import { Config } from '@remotion/cli/config';
Config.setVideoImageFormat('jpeg');
Config.setOverwriteOutput(true);
Config.setConcurrency(4);
schema.ts
import { z } from 'zod';
export const VideoProjectSchema = z.object({
title: z.string(),
duration: z.number().min(1000),
scenes: z.array(z.object({
id: z.string(),
duration: z.number(),
layout: z.any(), // Validate with LayoutSchema
cursor: z.any(), // Validate with CursorSchema
camera: z.any(), // Validate with CameraSchema
})),
});
export type VideoProject = z.infer<typeof VideoProjectSchema>;
Quick Start Guide
- Initialize Project: Run
npx create-remotion-app my-video-projectto scaffold a new Remotion project. - Install Dependencies: Add required packages:
npm install zod remotion @remotion/cli. - Define Scene: Create a new scene component with prop-driven layout and cursor logic.
- Run Studio: Execute
npm run studioto open the visual editor and preview your video. - Render Output: Use
npx remotion render src/index.ts ProductShowcase out/video.mp4to generate the final MP4.
Mid-Year Sale β Unlock Full Article
Base plan from just $4.99/mo or $49/yr
Sign in to read the full article and unlock all tutorials.
Sign In / Register β Start Free Trial7-day free trial Β· Cancel anytime Β· 30-day money-back
