Remotion Responsive Compositions: Portrait, Landscape, and Square from One Codebase
Remotion Responsive Compositions: Portrait, Landscape, and Square from One Codebase
Publishing video content across multiple platforms in 2026 means shipping three aspect ratios simultaneously: the classic 16:9 widescreen for YouTube and desktop, the 9:16 vertical format that dominates TikTok, Instagram Reels, and YouTube Shorts, and the 1:1 square that still performs well in Instagram feed and LinkedIn posts. If your team manages these three formats for every campaign, you know the pain of duplicating animation work, tweaking sizes manually, and then discovering that a change to the main creative means three rounds of edits.
Remotion solves this elegantly. Because compositions are just React components, the same component can adapt to any canvas size — you only need to write the animation logic once. This article walks through exactly how to do that: registering multiple compositions from a single component, using useVideoConfig() to drive dynamic layouts, calculating a unified scale from the canvas dimensions, and sizing text as a fraction of that scale so everything looks intentional regardless of the output format.
The Core Idea: One Component, Multiple Registrations
Remotion’s Root.tsx file is where you register all your compositions. Each <Composition> entry specifies a unique id, a component, and the canvas dimensions via width and height. The same component can appear multiple times with different dimensions.
// Root.tsx
import { Composition } from "remotion";
import { AdBanner } from "./AdBanner";
export const RemotionRoot = () => {
return (
<>
<Composition
id="AdBanner-Landscape"
component={AdBanner}
durationInFrames={150}
fps={30}
width={1920}
height={1080}
/>
<Composition
id="AdBanner-Portrait"
component={AdBanner}
durationInFrames={150}
fps={30}
width={1080}
height={1920}
/>
<Composition
id="AdBanner-Square"
component={AdBanner}
durationInFrames={150}
fps={30}
width={1080}
height={1080}
/>
</>
);
};
This is the entire foundation. Three compositions, one component. Now the challenge shifts to making AdBanner aware of which canvas it is rendering into — and that is precisely what useVideoConfig() is for.
Reading Canvas Dimensions with useVideoConfig()
The useVideoConfig() hook returns the width, height, fps, and durationInFrames of the currently rendering composition. Calling it inside any component gives you the live canvas size, whether you are previewing in Remotion Studio or rendering to a file.
import { useVideoConfig } from "remotion";
export const AdBanner: React.FC = () => {
const { width, height } = useVideoConfig();
const isPortrait = height > width;
const isSquare = width === height;
const isLandscape = width > height;
return (
<div style={{ width, height, position: "relative" }}>
{/* layout adapts below */}
</div>
);
};
This gives you a reactive boolean you can use for conditional layout decisions. But booleans alone are not enough — you also need a way to scale every visual element proportionally.
The Scale Calculation Pattern
The most useful pattern for multi-format compositions is computing a single scale number that represents the effective “unit size” of the canvas. The standard approach uses Math.min(width, height), which returns the shorter dimension. This ensures that on a 1080×1920 portrait canvas and a 1920×1080 landscape canvas, your scale unit is the same: 1080. Elements sized as a fraction of scale will appear physically similar in both outputs.
const { width, height } = useVideoConfig();
const scale = Math.min(width, height);
// Typography
const titleFontSize = scale * 0.08; // 8% of the shorter side
const bodyFontSize = scale * 0.04;
// Spacing
const padding = scale * 0.05;
// Icon or logo size
const logoSize = scale * 0.12;
For a 1080×1080 square, scale is 1080. The title renders at 86.4px. On a 1920×1080 landscape, scale is still 1080, so the title is still 86.4px — which looks correct because the shorter edge is the same physical dimension. On a 1080×1920 portrait, scale is again 1080, keeping consistent proportions.
This approach breaks only when you want an element to fill the longer axis — a full-bleed background image, for instance. In that case you use Math.max(width, height) or simply use 100% CSS values for elements that should stretch.
Building the Adaptive Layout Component
Here is a more complete example that combines all three ideas — useVideoConfig(), the scale calculation, and conditional layout based on orientation:
import { useCurrentFrame, useVideoConfig, interpolate, spring } from "remotion";
export const AdBanner: React.FC<{
headline: string;
subline: string;
logoSrc: string;
}> = ({ headline, subline, logoSrc }) => {
const frame = useCurrentFrame();
const { width, height, fps } = useVideoConfig();
const scale = Math.min(width, height);
const isPortrait = height > width;
// Shared animation values
const headlineOpacity = interpolate(frame, [0, 20], [0, 1], {
extrapolateRight: "clamp",
});
const headlineY = interpolate(frame, [0, 20], [30, 0], {
extrapolateRight: "clamp",
});
const logoSpring = spring({
frame,
fps,
config: { damping: 12, mass: 0.8 },
});
const logoScale = interpolate(logoSpring, [0, 1], [0.6, 1]);
return (
<div
style={{
width,
height,
backgroundColor: "#0f172a",
position: "relative",
display: "flex",
flexDirection: isPortrait ? "column" : "row",
alignItems: "center",
justifyContent: "center",
gap: scale * 0.04,
padding: scale * 0.06,
boxSizing: "border-box",
overflow: "hidden",
fontFamily: '-apple-system, "Segoe UI", Roboto, sans-serif',
}}
>
{/* Logo */}
<img
src={logoSrc}
style={{
width: scale * 0.18,
height: scale * 0.18,
objectFit: "contain",
transform: `scale(${logoScale})`,
flexShrink: 0,
}}
/>
{/* Text block */}
<div
style={{
display: "flex",
flexDirection: "column",
gap: scale * 0.02,
textAlign: isPortrait ? "center" : "left",
maxWidth: isPortrait ? "90%" : "60%",
}}
>
<h1
style={{
fontSize: scale * 0.08,
fontWeight: 700,
color: "#f8fafc",
margin: 0,
lineHeight: 1.2,
opacity: headlineOpacity,
transform: `translateY(${headlineY}px)`,
}}
>
{headline}
</h1>
<p
style={{
fontSize: scale * 0.04,
color: "#94a3b8",
margin: 0,
lineHeight: 1.5,
opacity: headlineOpacity,
}}
>
{subline}
</p>
</div>
</div>
);
};
The flexDirection switches between column (portrait, vertical stack) and row (landscape, side by side) based on orientation. Text alignment follows the same logic. All sizing is derived from scale, so the proportions are consistent across all three canvas sizes.
Passing Format-Specific Props with defaultProps
Sometimes you want each format to receive slightly different content — a tighter headline for portrait, a different background color for square, or an alternate logo variant. Remotion supports this through defaultProps on the <Composition> element.
<Composition
id="AdBanner-Portrait"
component={AdBanner}
durationInFrames={150}
fps={30}
width={1080}
height={1920}
defaultProps={{
headline: "Short hook for vertical",
subline: "Swipe up to learn more",
logoSrc: "/logo-white.png",
}}
/>
Combined with calculateMetadata, you can even derive the duration dynamically from the props — useful when the headline text length affects the hold time at the end of the animation.
Rendering All Three Formats from the CLI
Once your compositions are registered, rendering all three is a single-line pipeline:
npx remotion render src/index.ts AdBanner-Landscape out/landscape.mp4
npx remotion render src/index.ts AdBanner-Portrait out/portrait.mp4
npx remotion render src/index.ts AdBanner-Square out/square.mp4
Or, more practically, wrapped in a shell script that accepts an inputProps JSON file so campaign-specific data can be injected at render time:
#!/bin/bash
PROPS='{"headline":"Summer Sale","subline":"Up to 50% off","logoSrc":"/logo.png"}'
for FORMAT in Landscape Portrait Square; do
npx remotion render src/index.ts "AdBanner-$FORMAT" \
"out/campaign-$FORMAT.mp4" \
--props="$PROPS"
done
This is the production pattern: a single props file drives all three renders, outputs go into separate files, and the whole batch runs unattended.
Advanced: Using inputProps for Dynamic Content
For teams that publish frequently — daily social posts, A/B tested creatives, localized campaigns — the inputProps flag is essential. You store your creative copy in a JSON file and pass it in at render time, keeping the animation logic completely separate from the content data.
{
"headline": "New Collection Drop",
"subline": "Available exclusively online",
"accentColor": "#6366f1",
"logoSrc": "https://cdn.example.com/logo.png"
}
export const AdBanner: React.FC<{
headline: string;
subline: string;
accentColor: string;
logoSrc: string;
}> = (props) => {
// props injected from inputProps at render time
};
Your CI pipeline can generate the JSON from a CMS or spreadsheet, invoke the render script, and upload the resulting files — no human intervention required.
Handling Text Overflow Across Formats
One practical problem with multi-format compositions is that a headline that fits comfortably in landscape might overflow in portrait. A few approaches work well:
Fixed font size with text truncation: Set a maximum number of characters per format using defaultProps. Simple and predictable.
Responsive font size clamping: Use Math.max to set a minimum readable size even on small canvases:
const fontSize = Math.max(scale * 0.07, 28);
Line clamping with CSS: Apply WebkitLineClamp in the style object for multi-line truncation:
style={{
display: "-webkit-box",
WebkitBoxOrient: "vertical",
WebkitLineClamp: isPortrait ? 3 : 2,
overflow: "hidden",
}}
RenderComp Templates for Multi-Format Publishing
If you want to skip the boilerplate and start with production-ready multi-format compositions, RenderComp offers a library of Remotion templates built on exactly this pattern. Each template ships with pre-wired Landscape, Portrait, and Square composition variants, the scale calculation baked in, and inputProps schemas for common use cases like product ads, announcement cards, and event promotions. Visit rendercomp.com to browse the collection.
Summary
Building a responsive Remotion composition comes down to four steps:
- Register the same component as multiple
<Composition>entries inRoot.tsx, each with a differentwidth/height. - Read canvas dimensions inside the component using
useVideoConfig(). - Derive a
scalevalue withMath.min(width, height)and express all sizes as fractions of it. - Use conditional styles (especially
flexDirection) to adapt the layout for portrait vs. landscape orientations.
This pattern eliminates the most common source of multi-format video debt: separate files per format that diverge over time. One component, three outputs, one place to make changes.
FAQ
Q1: Can I use different durations for each format in the same component?
Yes. Each <Composition> registration specifies its own durationInFrames independently. You can also derive duration dynamically using calculateMetadata if the duration depends on input props.
Q2: Does useVideoConfig() work inside helper components, not just the root?
Yes. useVideoConfig() is a React context hook that is available anywhere in the render tree. You do not need to pass width and height as props down every level — call the hook wherever you need the values.
Q3: What is the best canvas size for 9:16 portrait on TikTok and Instagram Reels? 1080×1920 at 30fps is the safe standard. Some platforms accept 1080×1920 at 60fps, but 30fps keeps file sizes manageable and is universally supported. For YouTube Shorts, 1080×1920 at 60fps is preferred.
Q4: How do I handle background images that need to fill the full canvas regardless of orientation?
Use objectFit: "cover" on an <Img> component sized to width and height. The image will crop to fill — just make sure your subject is centered or use objectPosition to pin the focal point.
Q5: Can I share animation timing values across all three format compositions? Yes. Extract shared constants (frame counts, easing functions, color tokens) into a separate file and import them into your component. Since all three compositions use the same component, they all pick up the same timing logic automatically.
Q6: My text looks different sizes in Remotion Studio preview vs. the rendered video. Why?
Remotion Studio preview scales the canvas to fit your browser window. The actual pixel values in the rendered video are always the canvas dimensions you specified (e.g., 1920×1080). Size elements relative to scale rather than viewport units to ensure visual consistency between preview and output.
Q7: Is there a performance cost to registering many compositions in Root.tsx?
No. Compositions are lazy — they only render when you explicitly request a render of that specific composition ID. Having 20 compositions registered in Root.tsx does not slow down rendering of any individual one.
Now available
Get 1,000+ Remotion Templates
Pay once — no subscription. Lifetime updates. TypeScript-first.
View pricing →