Remotion for E-Commerce: Automate Product Videos at Scale
Remotion for E-Commerce: Automate Product Videos at Scale
Product videos drive conversion. According to mainstream e-commerce wisdom, shoppers who watch a product video are significantly more likely to complete a purchase — and social platforms like Instagram and TikTok actively boost video content over static images in their algorithmic feeds. The problem has never been whether product videos are worth making. The problem has always been scale.
A fashion brand with 3,000 SKUs cannot hire a video production team to shoot and edit individual clips for every product, every season, every sale event. Even with stock footage and templatised editing, the per-unit cost of a bespoke product video makes the math brutal for anything below your hero products.
Remotion changes this completely. Because Remotion composes video in React — driven by data rather than by hand — you can author a single template once and render it across your entire catalog automatically. The same file that generates a video for your midnight-blue chino can generate one for every other SKU, each with its own name, price, colour palette, and imagery, without opening a single editing application.
This guide walks through the full pipeline: structuring your product data, building a data-driven video template, adding price animations for flash sales, and batch-rendering 1,000 products programmatically.
The Core Mental Model: Data In, Video Out
A Remotion composition is a React component. Like any React component, it accepts props. Those props can come from anywhere — a static JSON file, a REST API response, a database query, or a spreadsheet export. The composition renders differently depending on the props it receives, frame by frame.
For e-commerce, this means your product catalog is your video script. Every field in your product record — name, price, image URL, category, sale flag — is a prop you can read and display inside the composition.
The high-level pipeline looks like this:
Shopify / WooCommerce API
↓
JSON export (one object per SKU)
↓
Remotion renderMedia() called per SKU
↓
MP4 / WebM per product (square, portrait, or widescreen)
No manual steps. No editing software. Just data flowing into a template and rendered video flowing out.
Structuring Your Product Catalog Data
Before writing any Remotion code, define a TypeScript interface for your product record. This serves as the contract between your data pipeline and your video template.
// types/Product.ts
export interface Product {
sku: string;
name: string;
brand: string;
price: number;
originalPrice?: number; // present when on sale
currency: string; // "USD", "JPY", "GBP", etc.
imageUrl: string;
category: 'apparel' | 'footwear' | 'accessories' | 'electronics' | 'home';
tags: string[];
isFlashSale: boolean;
flashSaleEndsAt?: number; // Unix timestamp
}
When you pull from Shopify’s REST API or GraphQL API, you will receive a different shape — normalize it to this interface before passing it to Remotion. A simple mapping function handles this:
// data/normalize.ts
import type { Product } from '../types/Product';
export function normalizeShopifyProduct(raw: ShopifyProduct): Product {
const variant = raw.variants[0];
const compareAtPrice = variant.compare_at_price
? parseFloat(variant.compare_at_price)
: undefined;
return {
sku: variant.sku,
name: raw.title,
brand: raw.vendor,
price: parseFloat(variant.price),
originalPrice: compareAtPrice,
currency: 'USD',
imageUrl: raw.images[0]?.src ?? '',
category: mapProductType(raw.product_type),
tags: raw.tags.split(',').map(t => t.trim()),
isFlashSale: raw.tags.includes('flash-sale'),
};
}
Export your entire catalog to an array of these objects — either as a static products.json file placed in Remotion’s public/ directory, or fetched at render time via inputProps.
Building the Product Video Template
With the data interface defined, you can build the composition. This template handles the full creative treatment: dynamic background colour by category, product image reveal, animated product name, price display, and an optional countdown for flash sales.
Category Colour Mapping
Different product categories warrant different visual treatments. Map each category to a colour palette:
// utils/categoryColors.ts
type ColorPalette = { bg: string; accent: string; text: string };
export const categoryColors: Record<string, ColorPalette> = {
apparel: { bg: '#1a1a2e', accent: '#e94560', text: '#ffffff' },
footwear: { bg: '#16213e', accent: '#0f3460', text: '#ffffff' },
accessories: { bg: '#2d132c', accent: '#ee4540', text: '#ffffff' },
electronics: { bg: '#0a3d62', accent: '#60a3bc', text: '#ffffff' },
home: { bg: '#2c3e50', accent: '#27ae60', text: '#ffffff' },
};
The Main Composition Component
// compositions/ProductVideo.tsx
import {
AbsoluteFill,
Sequence,
useCurrentFrame,
useVideoConfig,
spring,
interpolate,
Img,
staticFile,
} from 'remotion';
import type { Product } from '../types/Product';
import { categoryColors } from '../utils/categoryColors';
import { PriceDisplay } from './PriceDisplay';
import { FlashSaleCountdown } from './FlashSaleCountdown';
export const ProductVideo: React.FC<Product> = (product) => {
const { category, name, imageUrl, isFlashSale } = product;
const palette = categoryColors[category] ?? categoryColors.apparel;
return (
<AbsoluteFill style={{ background: palette.bg }}>
{/* Product image — enters with a scale spring */}
<Sequence from={0} durationInFrames={Infinity} name="ProductImage">
<ProductImageReveal imageUrl={imageUrl} accent={palette.accent} />
</Sequence>
{/* Product name — slides up from below */}
<Sequence from={15} durationInFrames={Infinity} name="ProductName">
<ProductNameEntry name={name} palette={palette} />
</Sequence>
{/* Price — fades in after name */}
<Sequence from={25} durationInFrames={Infinity} name="Price">
<PriceDisplay product={product} palette={palette} />
</Sequence>
{/* Flash sale countdown overlay (conditional) */}
{isFlashSale && (
<Sequence from={35} durationInFrames={Infinity} name="FlashSale">
<FlashSaleCountdown product={product} palette={palette} />
</Sequence>
)}
</AbsoluteFill>
);
};
Product Image Reveal
const ProductImageReveal: React.FC<{ imageUrl: string; accent: string }> = ({
imageUrl,
accent,
}) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const scale = spring({
frame,
fps,
from: 0.85,
to: 1,
config: { mass: 1, stiffness: 80, damping: 14, overshootClamping: true },
});
const opacity = interpolate(frame, [0, 12], [0, 1], {
extrapolateRight: 'clamp',
});
return (
<AbsoluteFill
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
paddingBottom: 200,
}}
>
<div
style={{
width: 480,
height: 480,
borderRadius: 24,
overflow: 'hidden',
transform: `scale(${scale})`,
opacity,
boxShadow: `0 32px 80px ${accent}55`,
}}
>
<Img
src={imageUrl}
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
/>
</div>
</AbsoluteFill>
);
};
Product Name Entry
const ProductNameEntry: React.FC<{
name: string;
palette: { accent: string; text: string };
}> = ({ name, palette }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const progress = spring({
frame,
fps,
config: { damping: 14, stiffness: 90 },
});
return (
<AbsoluteFill
style={{
display: 'flex',
alignItems: 'flex-end',
justifyContent: 'center',
paddingBottom: 180,
}}
>
<h1
style={{
fontFamily: '-apple-system, "Segoe UI", Roboto, sans-serif',
fontSize: 48,
fontWeight: 700,
color: palette.text,
opacity: progress,
transform: `translateY(${interpolate(progress, [0, 1], [30, 0])}px)`,
margin: 0,
textAlign: 'center',
maxWidth: 800,
padding: '0 40px',
}}
>
{name}
</h1>
</AbsoluteFill>
);
};
Price Animation: Flash Sale Countdown
A flash sale demands urgency. A static price tag does not communicate that. Remotion lets you animate the price itself — a countdown that shrinks the remaining time frame by frame, creating genuine tension.
// compositions/FlashSaleCountdown.tsx
import {
AbsoluteFill,
useCurrentFrame,
useVideoConfig,
spring,
interpolate,
} from 'remotion';
import type { Product } from '../types/Product';
interface FlashSaleCountdownProps {
product: Product;
palette: { accent: string; text: string };
}
export const FlashSaleCountdown: React.FC<FlashSaleCountdownProps> = ({
product,
palette,
}) => {
const frame = useCurrentFrame();
const { fps, durationInFrames } = useVideoConfig();
// Determine how far through the video we are (0 → 1)
const videoProgress = frame / durationInFrames;
// Simulate countdown: show remaining time shrinking across the video
// In production this would be driven by real time remaining data
const totalCountdownSeconds = 3600; // 1 hour flash sale
const remainingSeconds = Math.round(
totalCountdownSeconds * (1 - videoProgress * 0.1)
);
const hours = Math.floor(remainingSeconds / 3600);
const minutes = Math.floor((remainingSeconds % 3600) / 60);
const seconds = remainingSeconds % 60;
const enterProgress = spring({
frame,
fps,
config: { damping: 12, stiffness: 100, overshootClamping: true },
});
// Pulse effect on the accent bar — drives urgency
const pulseScale = 1 + interpolate(
Math.sin((frame / fps) * Math.PI * 2),
[-1, 1],
[-0.015, 0.015]
);
return (
<AbsoluteFill
style={{
display: 'flex',
alignItems: 'flex-end',
justifyContent: 'center',
paddingBottom: 40,
}}
>
<div
style={{
background: palette.accent,
borderRadius: 12,
padding: '12px 32px',
opacity: enterProgress,
transform: `scale(${pulseScale}) translateY(${interpolate(
enterProgress,
[0, 1],
[20, 0]
)}px)`,
}}
>
<div
style={{
fontFamily: '-apple-system, "Segoe UI", Roboto, sans-serif',
fontSize: 14,
fontWeight: 600,
color: 'rgba(255,255,255,0.8)',
textTransform: 'uppercase',
letterSpacing: '0.08em',
textAlign: 'center',
marginBottom: 4,
}}
>
Flash Sale Ends In
</div>
<div
style={{
fontFamily: '-apple-system, "Segoe UI", Roboto, sans-serif',
fontSize: 32,
fontWeight: 800,
color: '#ffffff',
letterSpacing: '0.05em',
textAlign: 'center',
}}
>
{String(hours).padStart(2, '0')}:{String(minutes).padStart(2, '0')}:
{String(seconds).padStart(2, '0')}
</div>
</div>
</AbsoluteFill>
);
};
Registering the Composition
In your Root.tsx, register the composition with the correct dimensions for your target platform:
// Root.tsx
import { Composition } from 'remotion';
import { ProductVideo } from './compositions/ProductVideo';
import type { Product } from './types/Product';
const SAMPLE_PRODUCT: Product = {
sku: 'PREVIEW-001',
name: 'Sample Product',
brand: 'RenderComp',
price: 89.99,
currency: 'USD',
imageUrl: 'https://example.com/product.jpg',
category: 'apparel',
tags: [],
isFlashSale: false,
};
export const RemotionRoot: React.FC = () => (
<>
{/* Square format — Instagram Feed / TikTok */}
<Composition
id="ProductVideo-Square"
component={ProductVideo}
durationInFrames={150} // 5 seconds at 30fps
fps={30}
width={1080}
height={1080}
defaultProps={SAMPLE_PRODUCT}
/>
{/* Portrait format — Instagram Reels / TikTok */}
<Composition
id="ProductVideo-Portrait"
component={ProductVideo}
durationInFrames={150}
fps={30}
width={1080}
height={1920}
defaultProps={SAMPLE_PRODUCT}
/>
</>
);
Batch Rendering 1,000 Products
The composition is ready. Now scale it. Remotion’s renderMedia() function from @remotion/renderer accepts inputProps — this is where you inject per-product data at render time.
// scripts/batchRender.ts
import { renderMedia, selectComposition } from '@remotion/renderer';
import { products } from '../data/products.json';
import path from 'path';
const BUNDLE_URL = '/path/to/your/remotion/bundle'; // build with bundleSite() first
async function renderProduct(product: Product, index: number) {
const composition = await selectComposition({
serveUrl: BUNDLE_URL,
id: 'ProductVideo-Square',
inputProps: product,
});
await renderMedia({
composition,
serveUrl: BUNDLE_URL,
codec: 'h264',
outputLocation: path.join('output', `${product.sku}.mp4`),
inputProps: product,
});
console.log(`[${index + 1}/${products.length}] Rendered ${product.sku}`);
}
async function runBatch() {
// Render in parallel batches of 4 to avoid exhausting system resources
const CONCURRENCY = 4;
for (let i = 0; i < products.length; i += CONCURRENCY) {
const batch = products.slice(i, i + CONCURRENCY);
await Promise.all(batch.map((p, j) => renderProduct(p, i + j)));
}
console.log('Batch render complete.');
}
runBatch().catch(console.error);
For very large catalogs (10,000+ SKUs), consider using @remotion/lambda to distribute renders across AWS Lambda functions. Each Lambda invocation can render one product in parallel, reducing a 1,000-product batch from hours to minutes.
Shopify to JSON Pipeline
If you use Shopify, the Admin REST API makes it straightforward to export your catalog to the format Remotion expects:
// scripts/fetchShopify.ts
async function fetchAllProducts(shopDomain: string, accessToken: string) {
const results: Product[] = [];
let url = `https://${shopDomain}/admin/api/2024-01/products.json?limit=250`;
while (url) {
const res = await fetch(url, {
headers: { 'X-Shopify-Access-Token': accessToken },
});
const data = await res.json();
results.push(...data.products.map(normalizeShopifyProduct));
// Follow pagination via Link header
const linkHeader = res.headers.get('Link') ?? '';
const nextMatch = linkHeader.match(/<([^>]+)>; rel="next"/);
url = nextMatch ? nextMatch[1] : '';
}
return results;
}
Run this script before your batch render to ensure the product data is always current. Wire the two scripts together in a CI/CD pipeline — GitHub Actions or a simple cron job — and your product video library updates automatically whenever your catalog changes.
Output Formats and Platform Specs
| Platform | Width | Height | Duration | Codec | Notes |
|---|---|---|---|---|---|
| Instagram Feed | 1080 | 1080 | 3–60s | H.264 | Square preferred |
| Instagram Reels | 1080 | 1920 | 3–90s | H.264 | Portrait only |
| TikTok | 1080 | 1920 | 3–60s | H.264 | Portrait only |
| 1000 | 1500 | Up to 15min | H.264 | Tall aspect | |
| YouTube Shorts | 1080 | 1920 | Up to 60s | H.264 | Portrait |
For batch rendering across multiple formats, extend the batchRender.ts script to loop over composition IDs:
const COMPOSITIONS = ['ProductVideo-Square', 'ProductVideo-Portrait'];
for (const compositionId of COMPOSITIONS) {
for (const product of products) {
// render each format for each product
}
}
FAQ
Q: Can Remotion fetch product images from a CDN at render time, or do they need to be local?
Remotion can load images from remote HTTPS URLs using the <Img> component (which behaves like a standard <img> but is aware of Remotion’s rendering pipeline). For production batch renders, ensure your CDN URLs are stable and publicly accessible. For the fastest and most reliable renders, download images locally first and reference them via staticFile().
Q: How do I handle products where the image aspect ratio is inconsistent?
Use CSS object-fit: cover inside a fixed-size container, as shown in the ProductImageReveal component above. This crops the image to fill the container regardless of the original aspect ratio, ensuring visual consistency across all SKUs.
Q: Is Remotion Lambda necessary for large batches, or can I run them locally?
Local rendering with renderMedia() is perfectly viable for batches up to a few hundred products on a modern machine. For 1,000+ SKUs, Remotion Lambda is worth the setup cost — it can parallelize renders across hundreds of Lambda functions simultaneously, reducing a several-hour local batch to minutes.
Q: How do I animate the price itself — for example, showing an original price struck through and a sale price appearing?
Use two separate <Sequence> components: one for the original price (visible early, fading out as the sale price enters) and one for the sale price (entering with a spring animation). The originalPrice field in the Product interface drives this logic conditionally.
Q: Can I add background music or product sound effects to each video?
Yes. Use the <Audio> component from remotion. Place your audio file in the public/ directory and reference it with staticFile(). You can vary the audio track based on category by mapping category to a track filename, just as you map category to a colour palette.
Q: How do I ensure the output video file size is acceptable for uploading to social platforms?
Pass a crf (Constant Rate Factor) value via the renderMedia() options. A CRF of 18 produces very high quality at reasonable file size. For platforms with strict file size limits, increase the CRF value (lower quality, smaller file) or reduce the resolution. The H.264 codec with a CRF of 23 is a good starting point for social media.
Q: Can I use this pipeline with WooCommerce instead of Shopify?
Yes. WooCommerce has a REST API that follows a similar structure. Replace the fetchShopify function with a WooCommerce API call and update the normalizeShopifyProduct mapping function to match WooCommerce’s response shape. The Remotion composition and batch rendering code remains identical.
Build Faster with RenderComp
The template architecture described in this guide is exactly how the product video templates in the RenderComp library are structured. Each template is a typed, data-driven composition with clean inputProps, multiple output format variants, and a documented batch rendering script.
If you want to skip the boilerplate and go straight to customising a production-ready template for your catalog, visit rendercomp.com. Browse the e-commerce collection and find a starting point that fits your brand — then adapt it with your own data pipeline and you will be rendering your full catalog in hours, not weeks.
Now available
Get 1,000+ Remotion Templates
Pay once — no subscription. Lifetime updates. TypeScript-first.
View pricing →