Shorts, Reels, TikTok from one source
Render 9:16, 1:1, and 16:9 from the same HTML composition. CSS handles the layout shifts.
One HTML file, three aspect ratios, three MP4s. Container queries swap the layout; the renderer just changes its viewport. No duplicate templates to keep in sync.
What you'll learn
- Driving aspect-ratio variants with
--widthand--height - Letting CSS container queries restructure the layout per format
- A single render command that emits all three formats
See it
Same content, two layouts, both deterministic.
The pattern
Author the composition in logical regions, not in absolute coordinates. Then let CSS rearrange those regions based on the container's shape.
<!doctype html>
<html>
<head>
<style>
body { margin:0; height:100vh; background:#0f172a; color:#fff;
font-family: ui-sans-serif, system-ui; }
.stage { container-type: size; height:100%; display:grid; gap:24px; padding:48px; }
/* Default: 16:9 horizontal — title left, media right */
.stage { grid-template-columns: 1fr 1fr; grid-template-areas: "title media"; }
.title { grid-area: title; align-self:center; font-size: 8cqi; }
.media { grid-area: media; }
/* 1:1 — stack with title on top */
@container (aspect-ratio < 1.3) and (aspect-ratio > 0.85) {
.stage { grid-template-columns: 1fr; grid-template-areas: "title" "media"; }
.title { font-size: 10cqi; text-align:center; }
}
/* 9:16 — vertical: title up top, media fills the rest */
@container (aspect-ratio < 0.85) {
.stage { grid-template-columns: 1fr; grid-template-rows: auto 1fr;
grid-template-areas: "title" "media"; }
.title { font-size: 12cqi; text-align:center; }
}
</style>
</head>
<body>
<div class="stage">
<h2 class="title">Three formats, one file.</h2>
<div class="media"><!-- chart, video, image, whatever --></div>
</div>
</body>
</html>Two things are doing the work: container-type: size on the stage, and cqi units on text so type scales with the container instead of the viewport.
Render all three
# 16:9 — YouTube, X
hyperframes render promo.html --out out/landscape.mp4 \
--width 1920 --height 1080 --crf 18
# 1:1 — Instagram feed
hyperframes render promo.html --out out/square.mp4 \
--width 1080 --height 1080 --crf 18
# 9:16 — Reels, TikTok, Shorts
hyperframes render promo.html --out out/vertical.mp4 \
--width 1080 --height 1920 --crf 18Or batch with a variants file:
[
{ "out": "out/landscape.mp4", "width": 1920, "height": 1080 },
{ "out": "out/square.mp4", "width": 1080, "height": 1080 },
{ "out": "out/vertical.mp4", "width": 1080, "height": 1920 }
]hyperframes render promo.html --variants formats.json --workers 6What about safe zones?
Each platform reserves chrome at the edges — TikTok's right rail, YouTube Shorts' caption strip. Bake those as CSS custom properties:
:root {
--safe-top: 220px;
--safe-bottom: 320px;
--safe-side: 32px;
}
.stage { padding: var(--safe-top) var(--safe-side) var(--safe-bottom); }Override per format with a --vars flag or a body class set by a wrapper script. Now your content never lands under a UI overlay.
When to break the one-file rule
If the vertical and horizontal versions need genuinely different content — different shot order, different captions, a different CTA — fork the template. Container queries are for layout shifts, not for content rewrites. The minute you have three @container blocks each hiding a different element, you have three templates pretending to be one.
Tweak it
- Use
aspect-ratioon hero media to keep it square inside whichever stage shape wraps it. - Set
text-wrap: balanceon titles — it pays for itself instantly on 9:16. - Render at 30fps for vertical (matches platform defaults) and 60fps for landscape (YouTube favors smoother motion).