Munchable has 16 vertical social reels, 1080 by 1920, each one showing a phone scanning a product and getting a verdict. They are rendered with Remotion, which means they are React components that produce mp4 files rather than a timeline in an editor.
That choice bought one property that I now think should be non-negotiable for any product that makes a factual claim in its advertising: there is no hand-typed "Good fit" anywhere in the compositions. Every verdict on screen is computed at render time by the same engine that runs in the app.
The verdict is a function call, not a design asset
/**
* Every verdict in a reel comes out of @munchable/rules-engine at render time.
* There is no hand-typed "Good fit" anywhere in the compositions, so the reels
* cannot promise something the app would not say.
*/
export function assess(def: ProductDef, profile: Profile): FitCheck {
const key = `${def.id}|${JSON.stringify(profile)}`;
let fit = cache.get(key);
if (!fit) {
fit = fitCheck(engineProduct(def), profile);
cache.set(key, fit);
}
return fit;
}
Each illustrated product in the cast carries a real ingredients list. The reel builds a profile, runs fitCheck, and renders whatever comes back: the verdict word, its colour, the per-condition rows, the reason line under them.
The reason this matters is not neatness. An ad is a promise with a long shelf life. Change a rule set six months from now so that the yoghurt in reel four is a caution rather than a good fit, and a hand-lettered video becomes a false claim about a health product, still live, still being served, with nothing in the build to tell you. With the engine in the render path, the next render either produces the new truth or produces a frame that visibly disagrees with the storyboard, and you find out while making the thing rather than in a complaint.
It also changes how the reels get written. You cannot storyboard a verdict you assumed. The story "two granolas both say gut friendly, one of them is an avoid" has to survive contact with the rule set: you write the ingredient lists, render, and read what the engine decided. When it disagrees, the storyboard is what changes.
The look is imported, and it used to be copied
The reels' theme file is mostly re-exports, and the comment at the top is a confession:
The palette, the scales and the verdict vocabulary are IMPORTED from
@munchable/design-tokens, the same module the mobile app re-exports, so a phone in a reel shows the real product. They used to be hand-copied here under a comment promising they mirrored the app, which is exactly the arrangement that drifts: changing the caution colour or the "Can't assess" label in the app left the reels rendering the old one with no type error.
"A comment promising they mirrored the app" is the load-bearing phrase. A copied constant with a comment saying where it came from is not a link to that place; it is a note about a link that no longer exists. Nothing enforces it, and nothing fails when it breaks.
What stays local to the reels is only what a video needs and an app does not: CSS font stacks, pixel line heights, and the dark card the camera picture sits in. Even the condition labels and the standing disclaimer come from the engine package, so the words in the video are the app's words.
Generic packs, real ingredient lists, and colours that are not allowed to mean anything
The illustrated cast is all invented brands, for the obvious reason that real packaging in a paid placement is a trademark problem. But the ingredient lists are real, because they are the input to the verdict.
The detail I like most is this one:
Pack colours stay off green and red so nothing on a shelf reads as a verdict.
In this product, green and red are not decoration, they are the vocabulary. A reel with a green pack sitting next to a red one has made a claim about both before the scan happens. So the palette for packs is sky, butter, blush, lavender, peach, biscuit, cocoa and gold, and the verdict colours stay reserved for verdicts.
If your product uses colour semantically, your marketing assets are part of that colour system whether you treat them that way or not.
Frames are the unit, and durations live with the storyboard
Each reel exports its own frame count next to the component, and the registry just wires them up:
<Composition id="trolley-run" component={TrolleyRun} durationInFrames={TROLLEY_RUN_FRAMES} {...size} />
<Composition id="hidden-onion" component={HiddenOnion} durationInFrames={HIDDEN_ONION_FRAMES} {...size} />
Length is a property of the story, so it lives in the file with the story. The registry never becomes the place where someone has to remember that reel nine is now four seconds longer.
Motion is shared the same way the palette is, as a small named vocabulary rather than magic numbers scattered across 16 files:
/** Springy pop for things arriving: overshoots a touch, like a cookie dropped on a plate. */
export const BOUNCY = { damping: 11, stiffness: 160, mass: 0.8 };
/** Arrive and settle, no overshoot. */
export const SETTLE = { damping: 18, stiffness: 140, mass: 0.9 };
/** A screen sliding in: quick, settles without bounce. */
export const SLIDE = { damping: 20, stiffness: 210, mass: 0.7 };
/** Quick and tight, for chips and small UI. */
export const SNAPPY = { damping: 14, stiffness: 240, mass: 0.6 };
plus four helpers: pop, ease, glide and lin. Sixteen reels built over many sittings still move like one product, because "a chip arriving" is always SNAPPY and never a number somebody re-guessed.
These are motion tokens, and they belong in the same conversation as colour tokens.
Remotion renders every frame from scratch, so memoize
A 25 second reel at 30fps is 750 renders of your component tree, potentially spread across parallel workers. Anything expensive that depends only on the story, not on the frame, has to be hoisted or cached, which is why assess memoizes on product and profile.
It is a good discipline even outside video: the frame number is the only thing that should change between one render and the next. If something else does, you have a bug that will show up as a flicker in one frame out of 750, which is a genuinely horrible thing to debug.
The render CLI validates ids and passes arguments
Small thing, real bug:
// Checked against the known compositions, and passed as ARGUMENTS rather than
// interpolated into a shell string. A typo now says so; an id with a space used
// to become two arguments and fail somewhere inside Remotion instead.
A mistyped composition id used to produce an error from inside the renderer with no obvious relationship to the typo. Now it fails immediately with the list of known ids. Any script that takes a name and shells out deserves the same two lines.
Silent on purpose
The reels render with no audio. Music is added later in an editor, and there are deliberately no sound effects, because the app has none either: scan and verdict sounds were removed from the product on purpose and the marketing is not allowed to reintroduce them.
Three of the reels cut on downbeats, so the README records the tempo range to lay under them rather than leaving it as folklore in someone's head. Captions stay inside the platform safe zone, nothing above 230px or below 1500px in the 1920px frame, so a platform's own UI never lands on a word.
The cast sheet is a composition, not a reel
There is a seventeenth composition that is not a reel. It renders every pack in the cast, front and back, so label copy can be proofread as a still.
Using your video framework to render your own proof sheet costs almost nothing and pays for itself the first time a pack's ingredient list has a typo that would otherwise have been found in the finished edit.
You can check the claims yourself
This is the part that makes the whole arrangement verifiable rather than just tidy. The reels, the mobile app and the public website all import the same engine, so a claim in a video has to agree with what the site says.
- A reel shows onion lighting up a pasta sauce as a problem for IBS. Is onion low FODMAP? is the same computation, printed as a page, with the engine's own reason line.
- Another turns the lactose sensitivity setting low, medium and high and watches one soft cheese change verdict. The same setting is what the condition guides describe, and public recipe pages are read at the strictest setting so that the site can never be softer than the app.
- A reel where two good fits go into a basket and the recipe list narrows is the same filtering you can see on the recipe library, where every "who this suits" line is computed rather than written.
- The hero animation on munchable.app is the same design tokens again, in a third medium: CSS rather than Remotion or React Native.
If you find a page on the site that contradicts a frame in one of the videos, that is a bug in one codebase rather than a difference of opinion between marketing and product, which is exactly the property we were buying.
Top comments (0)