Lower Thirds
Ten name-tags for the same guest. The whole point is that you can’t tell they came from one person.
A lower third is the little name-and-title card that slides in when someone starts talking on a podcast or an interview. Every show has them. Almost nobody has ten that feel like one system.
HyperFrames is HeyGen’s open framework for rendering video from HTML. Their catalog had a gap listed on the contributing page, in their own words: lower thirds, 10 variations for podcasts/interviews. So I built the ten. This is my first open-source contribution, now open as PR #2971.
Same guest, ten mechanics. Click a name to jump to it. Every clip runs the exact block that shipped in the PR.
01The gap
The repo already had two things called lower thirds: a YouTube subscribe overlay and a German news-tabloid style. Neither is a podcast name-tag, and neither is a set. That’s why the gap was still open. A show doesn’t need one more one-off. It needs a family it can pick from and swap between without the segment looking like it changed editors.
02One contract, ten mechanics
Every block reads the same set of CSS custom properties and the same data-lt-* attributes off one root. Name, title, affiliation, duration, side, theme, accent color. Type the guest in once and the block fills itself. Because all ten share the contract, they are drop-in interchangeable mid-edit. Point a loop at a CSV of guests and emit a whole season without touching the HTML.
The differences are all in the entrance. A bar wipes. A rule grows. A panel frosts in. A departure-board flips glyphs. A strip of tape slams past its mark and a title types out under it. Same skeleton, ten personalities.
03What makes it registry-grade
A registry block can’t just look right once. It has to render identically every time, forwards and backwards, on any machine. The catalog enforces five hard rules, and all ten clear them:
- Deterministic. No wall-clock reads, no unseeded randomness. The split-flap board uses a seeded PRNG, verified byte-identical across runs.
- One paused GSAP timeline per block, built up front.
- Registered under a known key that matches the block’s id and its directory name.
- No requestAnimationFrame. Everything is seek-driven, so a scrub backward lands exactly where it should.
- A hard kill at the end. Nothing lingers a frame past its clip.
Every id and class is prefixed with the block name, so ten of these can live in one document as sub-compositions and never collide.
04Contrast is a design constraint
Lower thirds sit over footage you don’t control. Bright, blown-out, whatever the camera gave you. So legibility isn’t a nicety here, it’s a pass/fail. Two decisions came out of that.
The accent color splits into two tokens. One stays saturated for bars, rules, and brackets, where contrast rules don’t apply. The other is a tint used only where the accent becomes actual text. The saturated red measures 1.57:1 as type over bright footage, which fails, so it never gets used that way. Every text layer clears WCAG AA. Worst case in the set is 4.54:1.
The four blocks that carry no panel ship a soft radial scrim behind the text instead. It fades in and out with the block so it never lands as a hard rectangle, and it exists specifically to hold contrast over the worst footage.
05Proving it
The whole set was verified locally before the PR. A Python build script generates all twenty files, and it regenerates them byte-identical every run. A harness loads each block, scrubs the timeline forward and back, and asserts it cleans up. It re-drives the attribute contract at different durations, proves the split-flap is deterministic, and runs all ten in one document to check for id collisions.
Running the real toolchain turned up one honest surprise: its layout inspector flags two blocks. I rendered both and looked. The renders are correct. The inspector mis-reads the semi-transparent scrim as a solid cover and the accent caret as invisible. False positives, documented in the PR rather than papered over, so the blocks ship untouched.
06The thread forward
Right now the insets and type scale are tuned for 1920×1080. Making them read from the composition’s own width and height is about twenty shared lines, and it would cover vertical social in the same set. That threads straight into LOG 013, Responsive Video, where one composition ships 9:16 and 16:9 from a single pass. Same idea, one layer down: stop shipping a decision, start shipping options.
Status: PR #2971 open on heygen-com/hyperframes, awaiting review. Flips to shipped on merge.