# Lower-Third Component — runnable code A complete, production-grade lower third you can drop in. Two paths: 1. **Remotion** (frame-deterministic, renders to MP4, batches over a roster). 2. **CSS/HTML** (no dependencies, for the web or a quick preview) with mask-reveal and wipe-bar variants. Both share the same design language: a left-anchored bar, a dominant name, a brand-accent role line, a staggered enter, a readable hold, and a faster exit. --- ## 1. Remotion component (props-driven, frame-deterministic) The animation is a pure function of `frame`, so it renders identically every time and stays in sync with the renderer. Enter staggers the bar, name, then role; exit is computed from the end of the clip so the title leaves faster than it arrived. ```tsx // LowerThird.tsx import { AbsoluteFill, Sequence, interpolate, spring, useCurrentFrame, useVideoConfig, } from "remotion"; // --- Brand lock: change once, every title matches ------------------------- export type Theme = { font: string; bar: string; // panel/bar color name: string; // primary text color role: string; // accent color for the role line radius: number; marginPct: number; // left/safe margin as % of width }; export const BRAND: Theme = { font: "Inter, system-ui, sans-serif", bar: "rgba(12,14,22,0.86)", name: "#FFFFFF", role: "#6EE7F0", // single brand accent radius: 10, marginPct: 5, }; export type LowerThirdProps = { name: string; role: string; theme?: Theme; holdSeconds?: number; // readable dwell, default 4s }; // Staggered enter via spring; exit via a quick fade/slide near the end. const useEnterExit = (delayFrames: number, holdSeconds: number) => { const frame = useCurrentFrame(); const { fps, durationInFrames } = useVideoConfig(); // ENTER: spring 0→1, offset by delayFrames (the cascade) const enter = spring({ frame: frame - delayFrames, fps, config: { damping: 200, stiffness: 180, mass: 0.6 }, }); // EXIT: faster than enter — last 10 frames of the clip const exitStart = durationInFrames - 10; const exit = interpolate(frame, [exitStart, durationInFrames], [1, 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp", }); const opacity = enter * exit; const x = interpolate(enter, [0, 1], [-32, 0]); // slide in from left return { opacity, x }; }; const Line: React.FC<{ delay: number; holdSeconds: number; children: React.ReactNode; size: number; weight: number; color: string; tracking?: number; }> = ({ delay, holdSeconds, children, size, weight, color, tracking = 0 }) => { const { opacity, x } = useEnterExit(delay, holdSeconds); return (
{children}
); }; export const LowerThird: React.FC = ({ name, role, theme = BRAND, holdSeconds = 4, }) => { const { width, height } = useVideoConfig(); const bar = useEnterExit(0, holdSeconds); // bar leads the cascade const left = (width * theme.marginPct) / 100; const baseline = height * 0.80; // sit ~80% down, inside title-safe return (
{/* panel behind text guarantees legibility over any plate */}
{name} {role}
); }; ``` Why each choice matters: - **`spring` per line, offset by `delay`** gives the bar→name→role cascade. Same offsets everywhere keep titles consistent. - **Exit derived from `durationInFrames`** means any clip length exits cleanly without per-instance tuning, and it's quicker than the enter (10f vs the spring's settle). - **`whiteSpace: nowrap` + left anchor** keeps long and short names pinned to the same edge — no awkward re-centering across a roster. - **`borderLeft` accent + semi-opaque panel** is the brand tie plus the legibility guarantee over unknown footage. ### Roster → many instances (one design, a whole panel) Sequence the same component down a list. Each person gets a slot; only the text changes. ```tsx // Roster.tsx import { Series } from "remotion"; import { LowerThird, BRAND } from "./LowerThird"; const ROSTER = [ { name: "Ada Lovelace", role: "Founder, Analytical Engines" }, { name: "Grace Hopper", role: "Rear Admiral, US Navy" }, { name: "Alan Turing", role: "Mathematician" }, ]; export const Roster: React.FC = () => ( {ROSTER.map((p) => ( ))} ); ``` ### Batch: one branded clip per person Make the roster an input prop and render once per row, so each speaker becomes a standalone overlay clip — identical animation and brand, only the text differs. ```bash #!/usr/bin/env bash # render one transparent-friendly lower-third clip per person in roster.json # roster.json: [{"name":"Ada Lovelace","role":"Founder, Analytical Engines"}, ...] set -euo pipefail mkdir -p out jq -c '.[]' roster.json | while read -r row; do slug=$(echo "$row" | jq -r '.name' | tr '[:upper:] ' '[:lower:]-') npx remotion render LowerThird "out/${slug}.mp4" --props="$row" done ``` For overlay-ready output with transparency, render the `LowerThird` composition (transparent background) to a codec that keeps alpha, e.g. `--codec=prores --prores-profile=4444`, or `--image-format=png` for a PNG sequence. --- ## 2. CSS / HTML version (no dependencies) The same design without a framework — for the web, OBS browser sources, or a fast look-test. Includes both a **slide+fade** default and a **mask-reveal** name, plus a **wipe-bar** variant. ```html
Ada Lovelace
Founder, Analytical Engines
``` ### Wipe-bar variant (broadcast feel) Swap the bar animation so a colored bar sweeps in and the text rides behind it: ```css .lt-bar.wipe { position: relative; overflow: hidden; animation: lt-in .45s cubic-bezier(.22,1,.36,1) both, lt-out .3s ease-in 4.5s forwards; } .lt-bar.wipe::before { content: ""; position: absolute; inset: 0; background: var(--accent); transform-origin: left; animation: lt-sweep .55s cubic-bezier(.76,0,.24,1) both; /* sweep across, then reveal */ } @keyframes lt-sweep { 0% { transform: scaleX(0); } 60% { transform: scaleX(1); } 100% { transform: scaleX(0); transform-origin: right; } } ``` The CSS clock (`animation`) is fine for live/web playback. For a deterministic MP4, prefer the Remotion path above — a frame renderer and CSS timers disagree, which causes flicker or desync at export. --- ## Quick reference | Need | Knob | |---|---| | Slower/faster reveal | enter duration 12–18f / `.5s` | | Longer dwell | `holdSeconds` / the `4.5s` exit delay | | Bigger name dominance | name `fontSize` (keep ≥1.6× the role) | | Rebrand everything | the `BRAND` theme object / `--accent` | | 9:16 placement | `baseline 0.80→0.70` / the media query | --- ## Built by the team behind iart.ai This skill is part of an open motion-graphics collection from iart.ai — the AI motion agent that turns data, scripts, and designs into editable motion graphics (Remotion → MP4). If you'd rather not hand-build this, iart.ai can generate branded lower-thirds from a list of names/titles, one-click — change the text/data and re-export. → [iart.ai](https://iart.ai/?utm_source=github&utm_medium=reference&utm_campaign=tiktok-video-skills&utm_content=ref_footer&utm_term=lower-thirds)