CommunityCoding & Developmentgithub.com

VirtualPirate/create-product-demo-video-skill

Claude Code skill: records a product demo video of a real user flow in your own web app — transcribed from the real UI, recorded to mp4/gif with Playwright + ffmpeg.

What is create-product-demo-video-skill?

create-product-demo-video-skill is a Claude Code agent skill that claude Code skill: records a product demo video of a real user flow in your own web app — transcribed from the real UI, recorded to mp4/gif with Playwright + ffmpeg.

Works with✓Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/VirtualPirate/create-product-demo-video-skill/tree/main/skills/create-product-demo-video

Installed? Explore more Coding & Development skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Ask in your favorite AI

Open a new chat with this agent skill pre-loaded.

Documentation

Build a demo video of the flow the user described. Output lands in a new demo/vN/ folder.

The one rule that matters: every screen is a transcription of the real UI, not an impression of it. A demo that invents layout, copy, or components is worse than no demo — it teaches viewers a product that doesn't exist. Read the source for each screen before writing any markup for it.

Inputs

/create-product-demo-video <what should happen> — a flow description. If it is vague ("show off the product"), pick the narrowest complete flow that ends in visible value, say which one you picked in one line, and build it. Don't block on questions.

Steps

  1. Map the flow to routes. Turn the prompt into an ordered list of real routes the user would pass through. Find the router first — a routes/ directory, an app/ tree, a <Routes> block, a route manifest — and list only paths that exist. If the flow needs an entity that must be created before a later screen can exist (a project before a project-scoped report), either include that screen or pick a scope that doesn't need it — don't skip a required step silently.

  2. Read the source for every screen. For each route: the route file, the components it renders, the app's shared layout components (shell, page header, empty state), and its design-system primitives (components/ui/ or wherever they live). Also read anything that generates strings the demo shows — preview helpers, derived names, backend render/template services. Copy strings verbatim; do not paraphrase product copy.

  3. Scaffold. mkdir -p demo/vN (next unused N — never overwrite an existing vN), then copy assets/skeleton.html → demo/vN/index.html and assets/record.mjs → demo/vN/record.mjs. assets/ sits next to this SKILL.md, wherever the skill is installed — a plugin cache dir, ~/.claude/skills/, or a project's .claude/skills/. Resolve it from the skill's own directory, never from the repo being demoed. The skeleton carries a placeholder shell, a token scale, and the playback engine. For a scored demo, also copy the chosen track from assets/music/ → demo/vN/music.mp3 — see Music. Copying it in (rather than referencing the skill folder) is what keeps vN/ re-recordable later, the same reason record.mjs is copied per version.

  4. Fit the skeleton to this product. Everything marked REPLACE: is a placeholder that must be swapped before any scene is written, because every screen inherits it:

    In index.htmlReplace with
    PRODUCTThe app's name, the host its address bar shows, and its real logo svg
    NAVThe sidebar transcribed from the real nav — same sections, same order, same labels
    TOPBARThe real topbar; delete controls the app doesn't have
    :root tokensThe :root block from the app's stylesheet, so colour/radius/type match
    ICONSPath data from the icon set the app actually uses
    Delete the optional CSS blocks (third-party screen, delivery panels) the flow doesn't use.
  5. Write the scenes. One <section class="scene" data-scene="N"> per screen, in order. Reuse the skeleton's CSS classes (.pagehead, .card, .btn, .seg, .opt, .chip, .scopeid, .statusbadge, .empty, .input, .pillbar, .toast) — they are shaped like the output of a Tailwind/shadcn-style system, so check each against the real component's classes and adjust rather than assume. Add new CSS only for a component the skeleton lacks, and derive it from that component's actual classes.

  6. Write play(). Title card → screens → outro. Use captions to name each step, and set CAPTIONS at the top of the script — the style, and steps = how many say() calls the run makes. Interactions go through the pointer helpers so the viewer can follow what's being clicked.

  7. Record. node demo/vN/record.mjs → demo/vN/out/product-demo.mp4. Add --gif for a 900px gif. A demo/vN/music.* file is picked up automatically; --music <path> points at one elsewhere, --music-db <n> shifts the level, --no-music forces silence.

  8. Verify by looking. Extract frames and read the images — this is not optional, it is how layout bugs get caught:

    for t in 5 12 20 28 36 44; do ffmpeg -y -loglevel error -ss $t \
      -i demo/vN/out/product-demo.mp4 -frames:v 1 /tmp/f$t.png; done
    

    Check: no oversized icons, nothing clipped at the fold, the pointer lands on the element it claims to click, text isn't collapsed onto one line. Fix and re-record until clean.

    Frames cannot show you the audio. On a scored demo, also check the stream and the ramps — volumedetect logs at info level, so -v error would swallow its output:

    ffprobe -v error -select_streams a:0 -show_entries stream=codec_name,duration \
      -of default=nw=1 demo/vN/out/product-demo.mp4
    for w in "-ss 0 -t 0.25" "-ss 12 -t 0.5" "-sseof -0.25"; do \
      ffmpeg -hide_banner -nostats $w -i demo/vN/out/product-demo.mp4 \
        -af volumedetect -f null - 2>&1 | grep mean_volume; done
    

    Expect an aac stream within ~0.05s of the video duration, max_volume below 0 dB, and the head and tail means well under the middle one — that is the fade proving it ran.

  9. Write demo/vN/README.md — scene table with routes, the source-file mapping, duration, the CAPTIONS style and music track the version was recorded with, and what the flow deliberately skips. Update demo/README.md to list the new version.

Report the path, duration and size, then open the mp4 so the user can watch it — open on macOS, xdg-open on Linux, start on Windows.

Engine API

Available inside play():

CallEffect
scene(n)Cross-fade to scene n, update the address bar from URLS
tap(sel, ms)Move pointer to element and click it (ripple + press)
moveTo(sel, ms) / click(sel)The two halves, when you need state changes between them
hideCursor() / place(x, y, 0)Reset the pointer when entering a scene
say(text, opts?) / hush()Caption in/out. { at: sel } anchors a coach caption; { step: n } forces the index
type(el, text, ms)Character-by-character typing into a <span>
wait(ms)Hold
reveal(el)Auto-called by moveTo — smooth-scrolls .main so off-fold targets are visible

Set window.__demoDone = true at the end (the skeleton already does) — the recorder waits on it.

Caption design

const CAPTIONS = { style: 'pill', steps: 6 } sits with URLS at the top of the engine. All four styles ship in the skeleton; say() is the same call whichever is on. Pick per video:

styleWhat it isPick it when
pill (default)Ink capsule, bottom centreAnything short and marketing-facing. Least occlusion, ~10 words per caption
lower-thirdLeft-anchored block with step index + progress trackLonger flows where the viewer should feel the sequence; most legible in a gif
rail68px band under the window, chapter dotsDocs and support clips where nothing may be covered. Costs app height — see below
coachCallout anchored to the element in play, rest dimmedFollow-along walkthroughs. Every caption needs { at: sel }; ones without fall back to pill

steps drives the index, the progress track and the rail's dots — count the say() calls and set it, or the run ends reading 05 / 06. Unsure which style? Open index.html in a browser and switch CAPTIONS.style — the page replays on R, so all four can be compared without recording.

Music

Optional, and off unless a track is there. The bed is muxed at encode time — Playwright captures no audio at all, so nothing about it touches the demo page or the engine.

node demo/vN/record.mjs                      # uses demo/vN/music.* if present, silent if not
node demo/vN/record.mjs --music ~/beds/x.mp3  # explicit path wins over the sibling
node demo/vN/record.mjs --music-db -4        # quieter bed
node demo/vN/record.mjs --no-music           # silent even with a sibling track
  • Bundled tracks live in assets/music/, one row per file in its LICENSES.md. No track ships without a verifiable CC0/CC-BY line — an unlicensed mp3 is everyone-who-installs-this's problem, not just yours. Unsure? Keep it off-repo and pass --music <path>.
  • Level is normalized, not set. Every track is driven to −18 LUFS with a −1.5 dB true-peak ceiling, so beds swapped between versions match each other without hand-tuning. --music-db shifts that target; it does not add gain on top.
  • Fades are automatic — 1.5s in, 2.5s out, the out timed off the real video duration.
  • Pick a track longer than the video. Anything shorter gets looped and the seam is audible; the recorder warns when it has to. 60s+ covers a normal 40–60s demo.
  • No vocals, no hard drops. The viewer is reading captions and UI copy at the same time.
  • Audio lands in the mp4 only. The webm is the raw silent capture, and a gif has no audio track at all — a music-only change is invisible in --gif output.

Conventions

  • Stage is a fixed 1280×800 mock browser; 44px of that is browser chrome, leaving 1280×756 for the app. The skeleton ships a 48

Related Skills