Two surfaces had already agreed on what a hero film is — full bleed, two masters picked by orientation, an SSR-correct still, reduced motion gets the ending, no loop — and the agreement lived in one app's components directory. The second surface that wanted one would have copied it, and the two copies would have drifted. So the decisions move here and the surfaces state only what is theirs: a prefix and a sentence. `<Frame src="/hero" alt="…" />` resolves the six rendered files by name, which makes a per-page film a matter of assets rather than of code. Rendering stays in hanzoai/frames. This package only plays what that one cuts.
3.0 KiB
@hanzo/frame
The film at the top of the page.
import Frame from "@hanzo/frame";
<Frame src="/hero" alt="What the film says, in one sentence." />;
That is the whole API. Two props, no options.
What it does
A hero film is not a <video> tag. It is a set of decisions that every surface
would otherwise make again, differently:
- Full bleed.
100svh,object-fit: cover, true black behind it. - Two masters, picked by orientation. A phone is 0.46 wide-to-tall and a
laptop is 1.6. Covering a 390×844 phone with a 16:9 master shows the middle
quarter of it. So there are two —
1080×1920and1920×1080— and the pick is by orientation, not width: a 768×1024 tablet is portrait, and the wide master would lose 555px off each side. Turning the phone re-picks. - The still is what the server sends. A
<picture>with media queries chooses it — no JavaScript, correct in SSR, right on the first paint. - Reduced motion gets the ending. That viewer never sees the film, so they
get the final frame — the finished product — and no
<video>is created at all. A paused player still downloads the megabytes. - No loop. The film ends on the product and stays there.
- The words live in
alt. Nothing is layered over the film, soaltis the message, for readers and for machines.
Motion viewers get frame 0 as the poster — the same picture the film opens on, so the swap to the player is invisible. Every viewer downloads exactly one of the four stills.
The six files
src is their shared prefix. src="/hero" means:
| File | What |
|---|---|
/hero-wide.mp4 |
1920×1080 master |
/hero-wide-first.jpg |
its frame 0 — the motion poster |
/hero-wide-last.jpg |
its final frame — the reduced-motion still |
/hero-tall.mp4 |
1080×1920 master |
/hero-tall-first.jpg |
its frame 0 |
/hero-tall-last.jpg |
its final frame |
Drop them in the app's public/. The names are the contract; there is no
mapping to configure.
Rendering the masters
Films are rendered locally, in headless Chrome, by Hanzo Frames — no keys, no credits, no service. That repo is the render half; this package is the display half. See its skills for composing a scene.
House rules for a product film:
- Film the real product. Real chrome, real copy, real numbers. Never invent a feature, a metric or a screen.
- Geist Sans and Geist Mono are the only two faces on a Hanzo surface
(
@hanzo/design). Monochrome, near-black#0a0a0apage on true black. - Calm pacing, gentle motion. Nothing swoops.
- End on the running product. Never on a URL, a wordmark or a call to action — the visitor is already there.
Adding one to a page
- Render the two masters and extract the four stills.
- Put all six in
public/under one prefix. <Frame src="/that-prefix" alt="…" />.
There is no step four, and no per-page component. A page that needs a film needs assets, not code.