Scriptable cinematic screen recorder for product demos — YAML in, polished MP4 out. Powered by Playwright CDP.
clipwise-recording.mp4
Generated with
npx clipwise demo— 1 YAML file, 248 lines, one command.
# No install needed — run straight from npx (your package.json stays untouched)
npx clipwise@latest demo # Try the built-in demo instantly
# Or create your own scenario
npx clipwise@latest init # Scaffolds .clipwise/ (single footprint dir)
# Edit .clipwise/scenarios/demo.yaml — change the URL to your site
npx clipwise@latest record .clipwise/scenarios/demo.yamlZero footprint: everything Clipwise touches lives in one .clipwise/ directory —
scenarios, fixtures, auth state, output. Remove every trace with rm -rf .clipwise.
npx clipwise@latest init— scaffolds.clipwise/with two ready scenariosnpx clipwise@latest record .clipwise/scenarios/keynote.yaml— renders a full keynote-style launch video with zero edits (it records the hosted demo dashboard)- Open
.clipwise/output/keynote.mp4— this is the quality bar you get out of the box - Edit
keynote.yaml: swap theurl:and selectors for your app, tweak the captions - Edit
brand.yaml: your accent color, font preset, catchphrases — the whole video follows
Prefer natural language? npx clipwise install-skill, then ask /clipwise in
Claude Code — it writes these YAMLs for you (the skill ships with the full
keynote recipe). For a plain screen recording instead of a launch video, start
from scenarios/demo.yaml and the YAML Scenario Format below.
- Node.js >= 18
- ffmpeg (for MP4 output)
- Chromium (auto-installed on first run via Playwright)
# macOS
brew install ffmpeg
# Ubuntu
sudo apt install ffmpeg
# Windows
choco install ffmpeg# Instant demo — records the built-in dashboard showcase
npx clipwise demo # Browser frame, MP4
npx clipwise demo --device iphone # iPhone mockup
npx clipwise demo --device android # Android mockup
npx clipwise demo --device ipad # iPad mockup
npx clipwise demo --url https://my-app.com # Your deployed site
# Record from YAML scenario (output defaults to .clipwise/output)
npx clipwise record <scenario.yaml> -f mp4
npx clipwise record <scenario.yaml> -f gif -o ./custom-dir
# Scaffold .clipwise/ (scenarios, fixtures, prepare assets, auth — one dir, zero footprint)
npx clipwise init
# Validate without recording
npx clipwise validate <scenario.yaml>import { ClipwiseRecorder, CanvasRenderer, encodeMp4, loadScenario } from "clipwise";
const scenario = await loadScenario("my-scenario.yaml");
const recorder = new ClipwiseRecorder();
const session = await recorder.record(scenario);
const renderer = new CanvasRenderer(scenario.effects, scenario.output, scenario.steps);
const frames = await renderer.composeAll(session.frames);
const mp4 = await encodeMp4(frames, scenario.output);Clipwise ships a built-in Claude Code skill. Once installed, type /clipwise in Claude Code to generate YAML scenarios, validate, and record — all through natural language.
npx clipwise install-skillThis copies the skill file to .claude/skills/clipwise.md (project-level if .claude/ exists, otherwise ~/.claude/skills/).
In any Claude Code session:
/clipwise
> Record a demo of my dashboard at http://localhost:3000
— click the login button, type credentials, navigate to analytics
Claude will:
- Generate a complete
clipwise.yamlscenario - Run
npx clipwise validateto check for errors - Run
npx clipwise recordto produce the MP4
Re-run npx clipwise install-skill after upgrading clipwise to get the latest skill.
Remove it anytime with npx clipwise install-skill --remove — the skill file is the
only thing Clipwise leaves outside .clipwise/.
A scenario has 4 sections: metadata, effects, output, and steps.
name: "My Demo"
description: "Optional description"
viewport:
width: 1280 # Browser width (default: 1280)
height: 800 # Browser height (default: 800)
effects:
# See "Effects" section below
output:
format: mp4 # gif | mp4 | png-sequence
width: 1280
height: 800
fps: 30 # 1-60
preset: social # social | balanced | archive
steps:
- name: "Step name"
actions:
- action: navigate
url: "https://example.com"
captureDelay: 200 # ms to wait after actions
holdDuration: 800 # ms to hold on result
transition: none # none | fade | slide-left | slide-up | blur
effects: # Per-step effects override (optional)
zoom:
enabled: false # Disable zoom for this step only| Action | Parameters | Default | Description |
|---|---|---|---|
navigate |
url, waitUntil? |
waitUntil: "networkidle" |
Navigate to URL |
click |
selector, delay?, timeout? |
Click an element | |
type |
selector, text, delay?, timeout? |
delay: 50 |
Type text (char-by-char, React/Vue compatible) |
hover |
selector, timeout? |
Hover over element | |
scroll |
y?, x?, selector?, smooth?, timeout? |
y: 0, x: 0, smooth: true |
Scroll by offset |
wait |
duration |
Wait (ms) | |
screenshot |
name?, fullPage? |
fullPage: false |
Capture marker |
| Action | Parameters | Default | Description |
|---|---|---|---|
waitForSelector |
selector, state?, timeout?, captureWhileWaiting?, displaySpeed? |
state: "visible", timeout: 15000 |
Wait for element state |
waitForNavigation |
waitUntil?, timeout?, captureWhileWaiting?, displaySpeed? |
waitUntil: "networkidle", timeout: 15000 |
Wait for page load |
waitForURL |
url, timeout?, captureWhileWaiting?, displaySpeed? |
timeout: 15000 |
Wait for URL match |
waitForFunction |
expression, polling?, timeout?, captureWhileWaiting?, displaySpeed? |
polling: "raf", timeout: 30000 |
Wait for JS expression to be truthy |
waitForResponse |
url, status?, timeout?, captureWhileWaiting?, displaySpeed? |
timeout: 30000 |
Wait for network response (URL substring match) |
smartWait |
until, selector?, timeout?, displaySpeed? |
until: "networkIdle", timeout: 30000, displaySpeed: 8 |
Smart wait — records real wait, auto-speeds in output |
captureWhileWaiting: When true, continuously captures frames during the wait (like smartWait). Useful for recording loading animations, progress bars, streaming responses. Pairs with displaySpeed (1-32, default: 8) to auto-compress wait frames in the output.
waitUntil options: "load", "domcontentloaded", "networkidle" (default)
state options: "visible" (default), "attached", "hidden"
polling options: "raf" (requestAnimationFrame, default) or milliseconds (e.g. 500)
# Wait for element to appear
- action: waitForSelector
selector: ".result-panel"
state: visible
timeout: 20000
# Wait for AI streaming response to complete
- action: waitForFunction
expression: "document.querySelector('.ai-response')?.dataset.done === 'true'"
timeout: 60000
# Wait for API response
- action: waitForResponse
url: "/api/chat/completions"
status: 200
timeout: 60000
# Wait for dynamic content length
- action: waitForFunction
expression: "document.querySelector('.output')?.textContent?.length > 100"
polling: 500For snappy demos (~30 seconds):
captureDelay: 50-100msholdDuration: 500-800mstype.delay: 15-25ms per character
For slower, cinematic demos:
captureDelay: 200-400msholdDuration: 1500-2500mstype.delay: 40-60ms per character
Record pages behind login by restoring a browser session. Supports Playwright's storageState file (recommended) or inline cookies.
# Option 1: Playwright storageState file (cookies + localStorage)
auth:
storageState: ./auth-state.json
# Option 2: Inline cookies
auth:
cookies:
- name: session_id
value: abc123
domain: .example.comGenerate a storageState file by logging in interactively:
npx playwright codegen --save-storage=.clipwise/auth/auth-state.json https://my-app.comTweak the page for the demo without touching your app code. Everything in
prepare is injected only into the recording browser — your source, build, and
database stay untouched, and all referenced files live in .clipwise/.
prepare:
# Hide elements that don't belong in a demo (cookie banners, dev overlays)
hide:
- "#cookie-banner"
- "[data-nextjs-toast]"
# Freeze the clock — dates render identically on every recording
freezeTime: "2026-06-10T09:00:00Z"
# Make Math.random deterministic — same charts/data every run
seedRandom: 42
# Seed web storage before the app boots (skip onboarding, set flags)
storage:
localStorage:
onboarding_done: "true"
# Mock API responses — demo data without seeding your database
mock:
- url: "/api/dashboard/stats" # URL substring match
fixture: ../fixtures/stats.json # relative to this YAML
- url: "/api/user"
body: { name: "Demo User" } # or inline
# Inject arbitrary CSS/JS for anything else
inject:
css: ../prepare/demo.css| Pressure to modify app code | Prepare replacement |
|---|---|
| Hide dev overlays / cookie banners conditionally | hide: |
| Build a "demo mode" with seeded data | mock: |
| Stub dates and randomness for consistent demos | freezeTime: + seedRandom: |
| Pre-complete onboarding for recordings | storage: |
| Blur sensitive data (emails, amounts) | mask: — element-level, follows scrolling |
Combined with freezeTime + seedRandom, recordings become deterministic —
the same scenario produces byte-identical frames run after run.
Declare a scenes: timeline and one clipwise record renders a complete
launch video: kinetic typography → footage vignettes (crop / push-in / split,
selector-anchored line annotations) → outro — connected by an ink thread that
travels across cuts.
viewport: { width: 1280, height: 800, deviceScaleFactor: 2 } # 2 = retina output
scenes:
- type: screen # footage take — recorded once, quoted by vignettes
id: demo
steps: [...] # the same steps you already know
- type: motion # kinetic typography (built-in templates)
template: kinetic-type
duration: 2200
props: { lines: "Ship *demos*,||not edits.", size: 86 }
- type: vignette # footage as a layer — camera is declarative
footage: demo
duration: 4200
layout: crop # hero | crop | split
label: "Smart Speed"
caption: "Loading compressed, *results crisp*"
crop: { selector: ".panel", pad: 14 } # selectors, not pixels
push: { from: 1.05, to: 1 }
start: { step: 3 } # quote from a step boundary
rate: 1.15
fx:
- { kind: circle, selector: "#revenue", delay: 2500 } # hand-drawn circle
- { kind: spotlight, selector: "#revenue", delay: 2500 } # dim everything else
# push: { from: 1.0, to: 1.2, origin: ".panel" } # match-cut toward a selector
# Optional — free BGM by URL (downloaded on your machine) + beat-synced cuts
audio: { file: "https://assets.mixkit.co/music/132/132.mp3", bpm: 120, volume: 0.32, fadeOut: 2000 }
# Optional — caption pills (timeline-absolute seconds), burned into every aspect
captions:
- { text: "Recorded from a real app", start: 0.4, end: 2.4 }Quality recipe (what makes the showcase videos look the way they do):
viewport.deviceScaleFactor: 2— retina-resolution capture (footage, type, everything)output.aspects: ["9:16", "1:1"]— reels/feed files from the same run (footage recorded once)prepare:— hide banners, freeze time, seed randomness, mock APIs.clipwise/brand.yaml— tone preset, accent, font preset (editorial= Inter + Fraunces), catchphrases; line annotations + the connecting thread switch on automatically- Structure: kinetic hook → hero push-in → close-up vignettes → interstitial → split (YAML × footage) → outro
The fastest path: install the Claude Code skill (npx clipwise install-skill)
and ask /clipwise in natural language — it generates this YAML for you.
All effects are optional and have sensible defaults.
Adaptive zoom follows cursor and zooms in on click targets.
zoom:
enabled: true
intensity: light # subtle | light | moderate | strong | dramatic
# 1.15x | 1.25x | 1.35x | 1.5x | 1.8x
# scale: 1.25 # Override with a numeric value instead of intensity
duration: 800 # Zoom animation ms
easing: spring # spring (natural) | ease-in-out (default)
autoZoom:
followCursor: true # Viewport pans to follow cursor position
transitionDuration: 300
padding: 200| Intensity | Scale | Best for |
|---|---|---|
subtle |
1.15× | Dense UIs, large viewports |
light |
1.25× | Loom-style gentle pull-in (default) |
moderate |
1.35× | Balanced — Camtasia range |
strong |
1.5× | Clear focus, some context sacrificed |
dramatic |
1.8× | Maximum emphasis, sparse UIs only |
Smart camera: Zoom is automatically suppressed during scroll actions to avoid disorienting motion. When followCursor is enabled, the focal point smoothly pans to follow cursor position (not just click targets). Zone-aware zoom (v0.7.0) merges nearby clicks into continuous zoom zones — no more jarring zoom-out/zoom-in between adjacent interactions. Spring easing (easing: spring) produces natural, Screen Studio-like camera motion with faster initial response and smooth deceleration.
Custom cursor with click ripple, trail, glow highlight, and speed control.
cursor:
enabled: true
size: 20
speed: "normal" # fast (~72ms) | normal (~144ms) | slow (~288ms)
clickEffect: true
clickColor: "rgba(59, 130, 246, 0.3)"
trail: true
trailLength: 6
highlight: true
highlightRadius: 35Gradient/solid padding with rounded corners and shadow.
background:
type: gradient # gradient | solid | image
value: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)"
padding: 48
borderRadius: 14
shadow: trueWraps the recording in a device mockup.
deviceFrame:
enabled: true
type: browser # browser | iphone | ipad | android | none
darkMode: true
url: "app.example.com" # address-bar display URL (default: localhost)| Type | Description |
|---|---|
browser |
macOS browser chrome with traffic lights |
iphone |
iPhone 15 Pro with Dynamic Island + home bar |
ipad |
iPad Pro with front camera dot |
android |
Android generic with punch-hole camera |
Displays a HUD at the bottom of the screen showing what was typed. By default, only modifier+key shortcuts are shown (industry standard — same as Screen Studio, KeyCastr, ScreenFlow). Set showTyping: true to also show regular typed text.
When typing across multiple input fields, each field gets its own line in the HUD (up to 3 recent sessions, oldest dimmed at top, newest bright at bottom). CJK text (Korean, Chinese, Japanese) is automatically wrapped to fit the screen width.
keystroke:
enabled: true
showTyping: true # show typed text (default: false — shortcuts only)
position: bottom-center
fontSize: 16
fadeAfter: 1500Text overlay at a corner.
watermark:
enabled: true
text: "Clipwise"
position: bottom-right
opacity: 0.5Automatically slows down near clicks and speeds up idle sections.
speedRamp:
enabled: true
idleSpeed: 2.0 # Skip factor for idle frames (default: 2.0)
actionSpeed: 0.8 # Slow factor near clicksContent-aware speed control. Automatically compresses loading/wait periods while keeping meaningful content at normal speed. Detects CSS loading spinners via CDP Animation domain — no configuration needed.
effects:
smartSpeed:
enabled: true
waitSpeed: 4 # Speed multiplier for wait/loading periods
idleSpeed: 4 # Speed multiplier for idle frames
transitionDuration: 500 # Ease-in/out duration (ms) — loader is visible before fast-forward
steps:
- name: "Submit form"
actions:
- action: click
selector: "#submit-btn"
- action: smartWait # Records real API wait, auto-compresses in output
until: selector # networkIdle | selector | domStable
selector: ".success-toast"
timeout: 30000
displaySpeed: 4 # 4x fast-forward (spinner visibly spins faster)How it works: smartWait records the actual wait (API calls, loading states) at full quality, then smartSpeed compresses those frames in the output. Loading spinners are auto-detected via CDP — frames during active @keyframes spin/rotate/pulse animations are automatically marked for compression.
Control how steps transition to each other.
steps:
- name: "Step 1"
transition: fade # none | fade | slide-left | slide-up | blur
actions: [...]| Transition | Description |
|---|---|
none |
Hard cut (default) |
fade |
Cross-dissolve between steps |
slide-left |
Outgoing frame slides left, incoming slides in from right |
slide-up |
Outgoing frame slides up, incoming slides in from bottom |
blur |
Outgoing frame blurs out while cross-fading to incoming |
Override global effects on a per-step basis. Any effect property can be overridden — unset properties inherit from the global config.
effects:
zoom:
enabled: true
intensity: light
steps:
- name: "Overview"
effects:
zoom:
enabled: false # No zoom for this step
actions: [...]
- name: "Detail view"
effects:
zoom:
intensity: strong # Extra zoom for this step only
actions: [...]Attach an audio file (MP3, WAV, etc.) to the output MP4.
audio:
file: "./narration.mp3"
volume: 1.0 # 0.0 - 2.0 (default: 1.0)
fadeIn: 0 # Fade-in duration in seconds
fadeOut: 0 # Fade-out duration in secondsMeasured on Apple M1 Max (10 cores) — Pulse Dashboard demo, 44s @ 30fps, 1280×800:
| Stage | v0.3.0 | v0.4.0 | v0.5.0 | v0.6.0 | v0.7.0 |
|---|---|---|---|---|---|
| Recording | 30.8 s | 31.1 s | 31.1 s | 31.1 s | 58.3 s¹ |
| Compose + Encode | 97.2 s | 60.6 s | 60.6 s | 60.6 s | 39.6 s |
| Total | 127.9 s | 91.7 s | 91.7 s | 91.7 s | 97.8 s¹ |
| Frames captured | 1,303 | 902 | 902 | 902 | 1,388 |
| ms/frame | 69 | 67 | 67 | 67 | 23 |
¹ v0.7.0 recording is longer due to more scenario steps (23 vs 20) and zoom-sustaining click events during typing. Compose throughput improved 3× (69 → 23 ms/frame) via Sharp pipeline batching.
Key optimisations in v0.4.0: concurrent streaming pipeline, static frame deduplication (~33% skipped), per-worker StaticLayers cache, and raw RGBA buffer pipeline.
v0.5.0 focuses on recording quality: smooth cursor, zoom intensity presets, multi-session keystroke HUD.
v0.6.0 focuses on convention alignment & expressiveness: gentler defaults (light zoom, normal cursor speed), per-step effects override, new transitions (slide, blur), audio narration, and smart camera (scroll-aware zoom suppression + cursor-following focal point).
v0.7.0 focuses on quality leap: spring physics zoom, zone-aware zoom continuity, focus point interpolation, smartWait + content-aware smartSpeed, auto loader detection (CDP Animation), HEVC 10-bit encoding (-tune animation), AV1 codec support, and Sharp pipeline batching (5→1 calls/frame).
Use the preset field to control quality and file size:
output:
format: mp4
fps: 30
preset: social # social | balanced | archive
codec: auto # auto | h264 | hevc | av1| Preset | libx264 CRF | HEVC VideoToolbox q:v | Target use case |
|---|---|---|---|
social |
22 | 60 | Twitter, LinkedIn, Loom-style sharing (~2-4 MB / 30s) |
balanced |
18 | 70 | General purpose, portfolio sites (~4-6 MB / 30s) |
archive |
13 | 80 | High-fidelity storage, source masters (uncapped) |
Recommended: preset: balanced for most demos.
Legacy:
quality: 1-100still works and maps to the nearest preset (>= 75→ social,>= 45→ balanced,< 45→ archive). Preferpresetfor clarity.
On Apple Silicon and Intel Mac, Clipwise automatically uses hevc_videotoolbox (HEVC/H.265) for hardware-accelerated encoding. This delivers ~5–10× faster encoding than software libx264 with no setup required.
macOS (HEVC VideoToolbox) → ~3 min wall time for a 44s, 1280×800 demo
Linux / Windows → ~8–12 min wall time (libx264, same quality)
VideoToolbox is detected at runtime — no config needed. If VideoToolbox is unavailable, it falls back to libx264 automatically.
For further compression after export:
# Re-encode with tighter settings
ffmpeg -i input.mp4 -c:v libx264 -crf 26 -preset slow -movflags +faststart output.mp4
# Convert to WebM (smaller, web-native)
ffmpeg -i input.mp4 -c:v libvpx-vp9 -crf 30 -b:v 0 output.webm-
Create your target page — any URL (localhost, file://, or remote)
-
Create a YAML file with your steps:
name: "My App Demo"
viewport:
width: 1280
height: 800
effects:
deviceFrame:
enabled: true
type: browser
background:
padding: 48
borderRadius: 14
output:
format: mp4
fps: 30
preset: social # social | balanced | archive
steps:
- name: "Open app"
captureDelay: 100
holdDuration: 1000
actions:
- action: navigate
url: "http://localhost:3000"
waitUntil: networkidle
- name: "Click login"
captureDelay: 50
holdDuration: 800
actions:
- action: click
selector: "#login-btn"
- name: "Type email"
captureDelay: 50
holdDuration: 600
actions:
- action: click
selector: "input[name=email]"
- action: type
selector: "input[name=email]"
text: "demo@example.com"
delay: 20- Record:
npx clipwise record my-scenario.yaml -f mp4 -o ./output- Use CSS selectors (
#id,.class,[data-testid=...]) for reliable targeting - Start each interaction with enough scroll to make the target element visible
- Use
waitUntil: "networkidle"for pages with API calls - Keep
type.delayat 15-25ms for a fast but readable typing effect - Use
transition: fadeortransition: blurbetween major sections for cinematic cuts
See PROMPTS.md for a ready-to-use prompt template. Copy-paste it to ChatGPT or Claude with your site URL, and get a working YAML scenario back.
Clipwise includes a documentation site and a live demo dashboard in the docs/ folder. To host it:
- Push to GitHub:
git push origin main - Go to Settings > Pages
- Set source to Deploy from a branch, select
main, folder/docs - Docs go live at
https://kwakseongjae.github.io/clipwise/ - Demo dashboard at
https://kwakseongjae.github.io/clipwise/demo/
The built-in npx clipwise demo already points to this URL by default.
- Selector validation: All CSS selectors in YAML are validated against a safe character allowlist
- URL handling: Only
http://,https://, andfile://schemes are accepted - Chromium sandbox: Playwright runs Chromium with default sandboxing
- Local processing: Recordings are processed locally — frames never leave your machine
npm install # Install dependencies
npm run build # Build with tsup
npm run typecheck # Type check
npm test # Run tests (vitest)MIT