--- name: remotion-aspect-ratios description: How to design ONE Remotion template that genuinely fits all three FlatRender aspects — 16:9, 1:1, 9:16 — without text cropping, off-screen elements, or a layout that is really just the 16:9 version letterboxed. Use whenever building or reviewing a template's layout. Read this BEFORE positioning any text or element. --- # Designing for 16:9 / 1:1 / 9:16 (do this right) Every template registers in all three aspects (`ASPECTS` in `src/lib/aspect.ts`). A common mistake (made in early FlatRender templates) is to design for 16:9 and just let the same coordinates render in 9:16 — which **crops text, pushes elements off-screen, and looks broken**. A template must be *re-laid-out* per aspect, not scaled. ## Two strategies — responsive component OR per-aspect components There are TWO legitimate ways to support the three aspects; pick per template: 1. **One responsive component** (default) — a single composition that adapts via `useLayout()` (`isWide/isSquare/isTall`, `pick()`). Use when the design is fundamentally the same and only positions/sizes change. Less code, stays in sync. 2. **A dedicated component per aspect** — when the design must differ STRUCTURALLY (different layout, different scene, different element set), not just reposition. e.g. a cinematic wide hero vs a stacked vertical story vs a centered square badge can be genuinely different scenes. The registry supports both. In `services/remotion/src/templates.tsx` a `TemplateDef` has `component` (shared default) plus an optional `componentsByAspect` map keyed by aspect id: ```tsx { id: "MyTemplate", component: MyTemplateWide, // fallback for any aspect not overridden componentsByAspect: { "1x1": MyTemplateSquare, // dedicated square design "9x16": MyTemplateTall, // dedicated vertical design }, schema, durationSec, defaultProps, // SHARED across aspects — keep the editable } // fields, props and duration identical ``` `Root.tsx` picks `componentsByAspect[aspectId] ?? component`. **Keep `schema`, `defaultProps`, and `durationSec` shared** so the studio shows the same editable fields and the same composition ids (`${id}-${aspect}`) regardless — only the visual layout differs. Reuse shared sub-components (background, characters, text overlay) across the per-aspect files so they don't drift. Guideline: start with one responsive component; split into per-aspect components only when responsive branching gets gnarly or the designs truly diverge. Don't duplicate three files when `pick()` would do. ## Why the naive approach breaks `useLayout().vmin(n)` sizes off the SHORT side (1080 in all three aspects), so a `vmin(92)` font is the same pixel size everywhere. But the WIDTH differs hugely: **1920px (16:9) vs 1080px (9:16)**. A headline that fits 1920 wide overflows/crops at 1080 wide. Likewise positioning at `width*0.34` puts an element in a totally different place relative to its own size when width changes. ## The rules 1. **Design 9:16 (tall) first.** It's the tightest. If it fits there, widening to 1:1 and 16:9 is easy. Building 16:9-first guarantees the tall version breaks. 2. **Branch layout on `L.isWide / L.isSquare / L.isTall`** — don't just scale. Things that sit side-by-side in 16:9 should STACK vertically in 9:16: ```tsx const L = useLayout(); // hero element position differs per aspect const heroX = L.isTall ? L.width * 0.5 : L.width * 0.34; // centered in tall, left in wide // layout direction flexDirection: L.isTall ? "column" : "row" ``` Add a tiny helper to `aspect.ts` and use it everywhere: ```ts pick: (wide: T, square: T, tall: T): T => kind === "wide" ? wide : kind === "tall" ? tall : square, ``` Then: `fontSize: L.pick(L.vmin(92), L.vmin(84), L.vmin(72))`. 3. **Cap font size to the WIDTH, not just the short side.** Headlines must wrap, never crop. Always set `maxWidth` and let text wrap: ```tsx maxWidth: L.width * 0.86, // safe text column // and scale type DOWN in tall: fontSize: L.pick(L.vmin(90), L.vmin(80), L.vmin(64)), wordBreak: "normal", lineHeight: 1.15, ``` Test with the LONGEST realistic Persian string for that field, not the short default. 4. **Respect SAFE ZONES.** Keep all meaningful content inside the central safe area; give tall more vertical margin: - 16:9: ~5% horizontal / 8% vertical padding. - 9:16: ~8% horizontal, and keep the hero in the middle 60% vertically (top/bottom of phones get UI chrome). Anchor text blocks to a zone (top third / bottom third), put the hero visual in the center. 5. **Reposition the hero per aspect.** A character/object that's at `x=34%` and text on the right in 16:9 should become hero-centered with text above/below in 1:1 and 9:16. Use `pick()` for x/y and for `justifyContent`/`alignItems`. 6. **Scale element COUNT/spread, not just size.** A row of 5 floating shapes that spans 1920 looks sparse/clipped at 1080 — reduce spread radius or count in tall (`L.pick(...)`). 7. **3D:** adjust `camera.fov` / `position.z` per aspect so the subject fills the frame (a tall frame needs the camera pulled back or a narrower fov). Keep the 2D text overlay using the same `pick()` rules. ## Mandatory verification (the step that was skipped before) Render a still in **all three aspects** at a frame where text is visible, with a LONG test string, and LOOK at each: ``` npx remotion still src/index.ts "-16x9" out/_a.png --frame=NN npx remotion still src/index.ts "-1x1" out/_b.png --frame=NN npx remotion still src/index.ts "-9x16" out/_c.png --frame=NN ``` Reject if: text is clipped at any edge, an element is off-frame, the hero is tiny/huge, or the tall version is obviously "the wide one squished". ## Checklist - [ ] Designed tall-first; used `pick()`/`isTall` to branch layout (not just scale). - [ ] Headlines wrap with `maxWidth`; tested with long Persian text — no cropping. - [ ] Hero repositioned/centered per aspect; content in safe zones. - [ ] Spread/count adjusted for narrow frames; 3D fov/camera tuned per aspect. - [ ] Eyeballed stills in ALL THREE aspects. Related: `../remotion-template-composition/SKILL.md`, `../remotion-design-styles/SKILL.md`.