Writing / Product
Sep 5, 2026
Introducing DemoKit
Dekai Li
· 4 min read
Today I'm releasing DemoKit, a command-line demo recorder. It reads your codebase, plans the demo, drives the real app in a browser, films it in 4K with the camera ported from Cap — and refuses to write the file unless the feature it was demonstrating actually worked.
Every demo video is a small lie of omission. You record the take where it worked, quietly delete the four where it didn't, and ship the fifth. Six weeks later the video still shows a green checkmark long after the feature underneath it stopped producing one. That's the failure mode DemoKit is built around, so the recorder carries the claim with it — this click was supposed to open the diff and show what changed — and when the take is over, three independent checks decide whether that happened.
The pipeline
DemoKit doesn't hunt through your UI hoping to find something worth filming. It reads the routes, the components and the tests first, writes a flow file, and only then opens a browser — the way a forward-deployed engineer learns a product before demoing it to the customer.
Four stages, one command, nothing to click
- 01 Plan
- Read the routes, the components and the tests; decide what the demo has to prove before opening anything.
- → flow.json
- 02 Capture
- Drive the real app in headless Chromium, signed in as you when it needs it; every press, release and repaint timestamped.
- → events.json
- 03 Render
- Cap's camera: spring zoom on the click, macOS cursor, your own wallpaper behind the window.
- → demo.mp4 · 4K
- 04 Verify
- Three checks per step; no file if they disagree.
- → verify.json
The camera
A model deciding where to push in is the worst of both worlds: slow, inconsistent, and wrong often enough that you stop trusting the output. So DemoKit doesn't decide. It ports Cap's camera whole — the same segment generation, the same spring solve, the same cursor — and lets the recorded clicks drive it.


Clicks arrive as press-and-release pairs, not as points. Cap anchors the end of a zoom segment to the release; feed it only the press and every segment comes out about 140 ms short, which is exactly the bug that made my first four attempts feel wrong without being obviously wrong. The camera position is then the analytic solution of a damped spring, evaluated every 8 ms. It never overshoots into a hard stop, and it never parks at 2× while nothing is happening.
The constants, as shipped by Cap
- PRE_PADDING
- 300 ms of runway before the press
- POST_PADDING
- 2 500 ms held after the release
- MERGE_GAP
- two segments closer than 2 500 ms become one move
- ZOOM_STEP
- the spring is solved every 8 ms, not every frame
- SCREEN_SPRING
- stiffness 200 · damping 40 · mass 2.25
- CURSOR_SPRING
- stiffness 470 · damping 3 · mass 70 — the mellow one
None of that is tuned to one site. Same binary, same command, pointed at somewhere nobody involved controls: typing into Wikipedia's search box and landing on the article about the exact spring the camera is running on.
en.wikipedia.org. The suggestion list is a real one,
and the article it opens describes underdamped, critically damped and overdamped motion
— the three cases the camera solves for. 11.7s · 2/2 verified.
Harder targets
A recorder that only survives server-rendered pages isn't much use. The apps worth demoing stream, virtualise, and draw themselves on a canvas. Three of the more hostile targets on the public web, driven and verified without a single fixture.
vercel/next.js, out to the issue tracker, into the open queue, and down into
the diff of whatever happened to be at the top that morning. Eight steps, eight camera
moves, 8/8 verified.Verification
Encoding succeeded, the click landed, the DOM moved — none of those mean the thing you were demonstrating works. So every step is judged three times, from three different vantage points, and all three have to agree before a file is written.
Three checks per step, from one real run
- STATE
-
Did the product change, and change into the right thing? Word-shingle novelty against the page before the click, plus the assertion the plan wrote down.
93.5% new · rows 0→6 · "Changes from" on screen - SOURCE
-
Did the pixels move, in the region it claimed? Measured on the raw capture, before any camera work.
33.19% of a 2880×1600 area at 0,20 — the floor is 0.40% - DELIVERED
-
Can you actually see it in the cut you're shipping? Sampled only where the camera is at rest, so a change mid-move or off-frame does not count.
10.21% between rest points · 8.77s of screen time
The interesting run is the one that fails. Here is the same Wikipedia flow with one extra beat that films a control which does nothing — clicking the tab the page is already on. Two steps prove themselves. The third can't, and so there is no file.
VERIFIED 2.27s search for the thing the camera is built on [dom] yes 7.5% of the page content is new, rows 1->12 [dom] yes "oscillating physical system" appears - on screen now VERIFIED 6.89s open the article [dom] yes 97.7% of the page content is new, url changed [dom] yes "critically damped" appears - on screen now FAILED 11.89s switch to the reading view [dom] NO 0.0% of the page content is new, rows 6->6, url same - below 8%, so nothing a viewer would notice happened here [dom] NO "From Wikipedia, the free encyclopedia" appears - it was already on screen before the step - this proves nothing [source] yes 0.40% of the window differs, in a 139x1057 area at 1717,160 outcome: failed ({"verified":2,"failed":1,"inconclusive":0})
Real output, unedited apart from two lines that pointed at file paths. Note the last check: the pixels did move. It still failed, because moving pixels is not the same as showing someone something.
Where it sits
This is a crowded shelf, and pretending otherwise would be the same dishonesty the tool is built to prevent. Vercel Labs ships WebReel, which records scripted browser demos from JSON with an animated cursor. aidemo is closer still: an agent writes a storyboard and it renders 1080p with auto-zoom, narration and captions. Both are good. Neither is trying to answer the question this one is.
I ran the closest one on the same errand as the video at the top, same day, same steps. WebReel
could not get past the search: its documented type shape errored on a
text/selector collision, and in two further variants the click landed but the page never
navigated, so it aborted with no video at all. DemoKit hit the same wall on the search
button, refused to write a file, named the step, and after the search was moved to the
suggestion click, verified all seven. Three attempts, zero videos, versus one refusal and then
7/7.
What proves the feature worked
recordVideoWebM VP8, no cursorasserts
the test; never looks at the video
WebReel (Vercel Labs)MP4/GIF, animated cursornothing
aidemo1080p, auto-zoom, narrationdiffs against a committed
baseline — drift, not brokenness
Skyvern / Browser Usesession replay, for debuggingvalidates
its own goal, then discards the recording
DemoKit3840×2160, real macOS cursor,
Cap's spring camerathree checks per step, and no file if they
disagree
Where it loses, plainly: no narration, no captions, no music. It is AGPL-3.0 because the camera
is ported from Cap, which is a real adoption cost next to MIT competitors. It cannot attach to
the Chrome you already have open — nothing can, since Chrome 136 — so it needs one
demokit login. macOS and Linux only. And it is one person's tool, not a company's.
Start recording
DemoKit is available now. One line, no Docker, no daemon, no account.
$ npm i -g @dekai/demokit
$ demokit local flows/amazon-task.json out/demo.mp4
verified 7/7 · out/demo.mp4 · 3840×2160
First run downloads a headless Chromium shell (~200 MB) and, only if your Python is missing Pillow or numpy, builds a venv in the cache. After that there is nothing to start and nothing to keep running. Every flow on this page ships in the repo, so you can run the exact recordings above against the exact same URLs.
Cap's MIT exception covers only its cap-camera* and scap-*
crates; crates/rendering is AGPLv3, and so is DemoKit. Versions 0.1.0 through
0.3.0 shipped under MIT by mistake and are deprecated on npm. The
NOTICE names every ported function and constant against the Cap file it
came from. Source on GitHub, package on
npm.
Filed under: ProductAuthor: Dekai Li