# 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)