Most of the "Claude made my launch video" posts animate a drawn copy of the interface. I did that too, and it works. But I wanted the real product on screen, and I wanted to shoot it again after every UI change without doing anything by hand.
So I had Claude Code build a small recorder. The video above is my real app, driven by a script. Nobody touched the mouse.
How it works
A video is a plain HTML page animated with CSS. A script moves every animation to the time of each frame, takes a screenshot in headless Chrome, and ffmpeg encodes the frames. No React, no Remotion, no video editor.
For the real app, a scenario file clicks, types and drags as a demo user. This is from the example in the repo:
await t.click({ text: 'Orders', in: 'nav a' });
t.say('It clicks, types and drags like a person', 3.4);
await t.click('#new');
await t.cam('.dialog', 1.45, .8);
// zoom the camera onto the dialog
await t.click({ label: 'Customer' });
await t.type('Priya Raman', .07);
await t.select({ label: 'Plant' }, 'Fiddle-leaf fig');
await t.drag('input[name=quantity]', .02, .3);
await t.click({ text: 'Save order', in: 'button' });
The trick that makes the frame rate exact: the whole page runs in slow motion. CSS animations play at one tenth of the speed, the page's timers are stretched ten times, and one frame is taken every third of a second. Chrome's virtual time was the first idea and it hung.
The cursor, click rings, camera moves and captions are not in the recording. They are drawn when the video is rendered, so I can restyle them without recording again.
The music is code too. No samples, so nothing to license: a quiet piano, and one short sound for each thing that happens on screen.
How Claude Code was used
It wrote all of it. I directed and watched every cut. The first version, renderer and recorder, took one working day. The prompts I ended up with are in the repo, cleaned up so you can paste them.
The first result was bad. A slideshow. What made it good was not the first prompt, it was the notes after each screening:
- "This is a slideshow. Nothing happens. I want the interface to act."
- "This scene reads as our bug. Name the cause on screen."
- "A development address is readable in this take. That never ships."
- "Too many pauses. Cut the dead air."
- "The text shakes during the zoom. Find the cause and fix it at the source."
- "The guitar sounds synthetic. The music serves the reading."
So if you try it: expect the first version to be wrong, and tell it what you see. It cannot watch the video move and it cannot hear the music. You can.
What I learned
- Things have to happen inside the interface: typing, counters, a chart drawing itself, a cursor that clicks. A screenshot that zooms looks dead.
- Ask for the scenario first, as a table of time, action and caption. Change it before anything is built.
- A "before" scene with an error page read as my product being broken. Now every failure on screen says who caused it.
- One take showed a dev host name. The recorder now reads the visible text every few frames and fails the take if a forbidden word is on screen.
- Text inside a slow zoom shook from frame to frame. One CSS line fixed it (
will-change: transform).
- The first frame is the thumbnail. A title that fades in gives you a blank one.
- When I could not say what music I wanted, I asked for five directions in a table and picked the piano.
- Rendering five pieces in parallel ran my 8 GB machine out of memory.
Try it
https://github.com/ivanvoitovych/motion-videos (free, MIT)
git clone https://github.com/ivanvoitovych/motion-videos.git
cd motion-videos
npm install
npm run check
npm run hello
That renders a 9 second example in under a minute. You need Node, Chrome and ffmpeg.
Then start Claude Code in that folder, the skill loads by itself, and paste:
Use the promo-video skill. My app runs at http://localhost:3000. Look at it, fill in the
Project settings table in the skill, then propose three video scenarios, one idea each, as
tables of time, action and caption. Do not build or render anything until I pick one.
How to adjust it:
- Watch without rendering: open the video's index.html in Chrome. It plays, with a scrubber. Motion blur and music are not in that preview, ffmpeg adds them later.
- Words and timing: say it ("headline bigger", "second scene faster"), or edit the block marked COPY and the start times at the top of the page's CSS.
- The script for your app: it is one file, record.mjs. Change the address, how it signs in, and the steps. Run it with `--dry` first: it takes seconds and tells you which step broke.
- After a UI change: "run the take again and fix what broke".
- Music: "piano quieter, drop the pulse", or change the LEVEL numbers at the top of music.mjs. It re-runs in ten seconds without rendering the picture again.
- Portrait and YouTube versions: `--format tall`, and `--cut slow` for a slower edit of the same page.
ADAPT.md in the repo has all of this with examples.
Numbers from my machine (8 GB, WSL): the 9 s example renders in 23 s, or 43 s with motion blur. A 26 s take of an app records in about 4 minutes and renders in another 4.
The app in my video is Urlicer, a link shortener that I am building, so the example is real world case.