
Webreel
- 132 installs
- 903 repo stars
- Updated April 11, 2026
- vercel-labs/webreel
webreel tool creating scripted browser demo videos with cursor animation and sound from JSON config.
About
webreel skill for recording scripted browser demo videos. Generates MP4, GIF, or WebM output from a JSON configuration defining browser actions, cursor paths, keystroke overlays, and sound effects. Automates product demo creation for documentation, marketing, and onboarding content without manual screen recording. Supports choreographed interactions that highlight UI flows with polished cursor animation and audio cues, producing shareable media assets from declarative scripts rather than live capture sessions.
- Scripted browser demos recorded to MP4, GIF, or WebM
- JSON config drives cursor animation and keystroke overlays
- Sound effects synchronized with browser interactions
- Automated product demo creation without manual recording
- Declarative scripts replace live screen capture workflows
Webreel by the numbers
- 132 all-time installs (skills.sh)
- +7 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #755 of 1,340 Generative Media skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Jul 27, 2026 (Skillselion catalog sync)
webreel capabilities & compatibility
- Capabilities
- script browser demo · render mp4 gif webm · add cursor overlays
- Use cases
- video generation · copywriting
What webreel says it does
Create and record scripted browser demo videos with webreel.
npx skills add https://github.com/vercel-labs/webreel --skill webreelAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 132 |
|---|---|
| repo stars | ★ 903 |
| Security audit | 1 / 3 scanners passed |
| Last updated | April 11, 2026 |
| Repository | vercel-labs/webreel ↗ |
How do I create a polished browser demo video without manual screen recording?
Create scripted browser demo videos as MP4, GIF, or WebM with cursor animation, keystroke overlays, and sound effects from JSON config.
Who is it for?
Developers creating product demos, tutorials, or release videos from browser scripts.
Skip if: Live streaming or non-browser video production workflows.
When should I use this skill?
User wants scripted browser demo videos, webreel recordings, or MP4 GIF WebM demos.
What you get
MP4, GIF, or WebM demo video generated from JSON script with cursor and keystroke overlays.
Files
webreel
webreel records scripted browser demos as MP4, GIF, or WebM with cursor animation, keystroke overlays, and sound effects. You define steps in a JSON config, and webreel drives headless Chrome, captures frames, and encodes with ffmpeg.
Installation
Install webreel as a project dependency so the version is pinned in the lockfile. This ensures deterministic recordings across machines and CI.
npm install webreelIf the project already has webreel in its dependencies, skip this step.
Prerequisites
webreel requires Chrome and ffmpeg, but you do NOT need to install them manually. Both are automatically downloaded to ~/.webreel on first run if not already present. Do not install Chrome or Chromium via puppeteer, playwright, or any other tool. webreel manages its own browser.
To download dependencies explicitly, or to fix corrupted/broken binaries:
npx webreel install
npx webreel install --force # delete cached binaries and re-downloadTo override the auto-downloaded binaries, set these environment variables:
CHROME_PATH- path to a Chrome or Chromium binary (used for preview)CHROME_HEADLESS_PATH- path to a chrome-headless-shell binary (used for recording)FFMPEG_PATH- path to an ffmpeg binary
If a recording fails with "No inspectable targets" or similar browser errors, the issue is almost certainly in the webreel config (wrong waitFor, missing element, timing), not a missing browser. Check the config and use --verbose to debug.
.gitignore
The .webreel directory is created at the project root during recording (frames, intermediate files). Add it to .gitignore:
.webreelQuick start
# Scaffold a config
npx webreel init --name my-demo --url https://example.com
# Edit webreel.config.json with your steps
# Preview in a visible browser (no recording)
npx webreel preview my-demo
# Record the video
npx webreel record my-demonpx resolves to the locally installed version when webreel is in devDependencies. Output lands in videos/ by default (configurable via outDir).
CLI commands
init
Scaffold a new webreel.config.json.
webreel init
webreel init --name login-flow --url https://myapp.com
webreel init --name hero -o hero.config.jsonFlags: --name (video name), --url (starting URL), -o, --output (output file path).
record
Record one or more videos.
webreel record # all videos in config
webreel record hero login # specific videos by name
webreel record -c custom.config.json # custom config path
webreel record --watch # re-record on config change
webreel record --verbose # log each step
webreel record --dry-run # print resolved config only
webreel record --frames # save raw JPEGs to .webreel/frames/preview
Run steps in a visible browser without recording.
webreel preview
webreel preview hero --verbosecomposite
Re-apply overlays (cursor, HUD, sfx) to existing raw video without re-recording. Useful for tweaking theme settings.
webreel composite
webreel composite heroinstall
Download Chrome and ffmpeg to ~/.webreel. Also use this to fix corrupted or broken binaries.
webreel install
webreel install --force # delete cached binaries and re-downloadvalidate
Check config for errors without running anything.
webreel validate
webreel validate -c custom.config.jsonConfig structure
Config files are auto-discovered as webreel.config.json (or .ts, .mts, .js, .mjs). Use -c to specify a custom path.
Top-level fields
| Field | Default | Description |
|---|---|---|
$schema | - | "https://webreel.dev/schema/v1.json" |
outDir | "videos/" | Output directory for rendered videos |
baseUrl | "" | Base URL prepended to relative video URLs |
viewport | 1080x1080 | Default viewport { width, height } |
theme | - | Cursor and HUD overlay theme |
sfx | - | Sound effect settings |
include | - | Array of step file paths prepended to all videos |
defaultDelay | - | Default delay (ms) appended after each step |
clickDwell | - | Cursor dwell time (ms) before a click |
Per-video fields
Each entry in the videos map supports:
| Field | Default | Description |
|---|---|---|
url | required | URL to open (absolute or relative to baseUrl) |
viewport | inherited | Override viewport { width, height } |
zoom | - | CSS zoom factor |
waitFor | - | Selector or text to wait for before starting steps |
output | "<name>.mp4" | Output path (.mp4, .gif, .webm) |
thumbnail | { time: 0 } | Thumbnail config, or { enabled: false } |
include | inherited | Step files to prepend |
theme | inherited | Override theme |
sfx | inherited | Override sound effects |
defaultDelay | inherited | Override default delay |
clickDwell | inherited | Override click dwell |
fps | 60 | Frame rate |
quality | 80 | Encoding quality (1-100) |
steps | required | Array of step objects |
Videos map
Videos are keyed by name in the config:
{
"videos": {
"hero": { "url": "...", "steps": [...] },
"login": { "url": "...", "steps": [...] }
}
}Record specific videos by name: webreel record hero login.
Step types
Each step has an action field. Most steps accept optional label, delay (ms after step), and description fields.
| Action | Key fields | Purpose |
|---|---|---|
pause | ms | Wait for a duration |
click | text or selector, within, modifiers | Click an element |
type | text, selector, within, charDelay | Type text into an input |
key | key, target | Press a key combo (e.g. "cmd+s") |
drag | from, to (element targets) | Drag between two elements |
scroll | x, y, selector | Scroll the page or an element |
wait | selector or text, timeout | Wait for an element to appear |
moveTo | text or selector, within | Move cursor to an element |
navigate | url | Navigate to a new URL |
hover | text or selector, within | Hover over an element |
select | selector, value | Select a dropdown value |
screenshot | output | Capture a PNG screenshot |
For full field details on every step type, see steps-reference.md.
Element targeting
Many steps target elements using these fields:
text- match by visible text contentselector- match by CSS selectorwithin- narrow the search to a parent matching this CSS selector
You can use text or selector (not both). within is optional and scopes the search.
{ "action": "click", "text": "Submit" }
{ "action": "click", "selector": "#submit-btn" }
{ "action": "click", "text": "Submit", "within": ".modal" }Viewport presets
Use preset names as string values for viewport, or specify { width, height }:
desktop (1920x1080), desktop-hd (2560x1440), laptop (1366x768), macbook-air (1440x900), macbook-pro (1512x982), ipad (1024x1366), ipad-pro (834x1194), ipad-mini (768x1024), iphone-15 (393x852), iphone-15-pro-max (430x932), iphone-se (375x667), pixel-8 (412x915), galaxy-s24 (360x780).
Theme
Customize cursor appearance and keystroke HUD:
{
"theme": {
"cursor": {
"image": "./cursor.svg",
"size": 32,
"hotspot": "center"
},
"hud": {
"background": "rgba(30, 41, 59, 0.85)",
"color": "#e2e8f0",
"fontSize": 48,
"fontFamily": "\"SF Mono\", monospace",
"borderRadius": 12,
"position": "top"
}
}
}cursor.image- path to a custom cursor SVG or PNGcursor.size- cursor size in pixelscursor.hotspot-"top-left"(default) or"center"hud.position-"top"or"bottom"
Common patterns
Shared steps via include
Factor out reusable step sequences (e.g. dismissing a cookie banner) into JSON files:
// steps/dismiss-banner.json
{
"steps": [
{ "action": "wait", "selector": ".cookie-banner", "timeout": 5000 },
{ "action": "click", "selector": ".accept-btn", "delay": 300 }
]
}Reference them in the config:
{
"include": ["./steps/dismiss-banner.json"],
"videos": { ... }
}Multiple videos in one config
Define several videos in the videos map. Shared settings (viewport, theme, defaultDelay) are inherited from the top level.
Environment variables
Config values support $VAR and ${VAR} substitution from the environment.
Output formats
Set the output extension to control format: .mp4 (default), .gif, .webm.
{ "output": "demo.gif" }Tips
- Always set
waitForon a video to ensure the page is ready before steps run. - Use
delayon individual steps to control pacing between actions. - Use
--watchduring development for automatic re-recording on config changes. - Use
compositeto iterate on theme/overlay settings without re-recording. - Use
--verboseto debug step execution. - Use
--dry-runto inspect the fully resolved config (includes, env vars, defaults). - Use
zoomto scale up small UIs for readability in the recording. - Start with
previewto verify steps work before committing to a full recording.
Reference files
- steps-reference.md - detailed docs for all 12 step types
- examples.md - annotated config examples for common use cases
webreel config examples
Minimal hello-world
The simplest possible config. Navigates to a page, waits for it to load, then clicks a link.
{
"$schema": "https://webreel.dev/schema/v1.json",
"videos": {
"hello-world": {
"url": "./web/index.html",
"viewport": { "width": 1920, "height": 1080 },
"zoom": 2,
"waitFor": ".cta",
"steps": [
{ "action": "pause", "ms": 500 },
{ "action": "click", "selector": "a.cta", "delay": 1000 }
]
}
}
}Form filling
Type into inputs with character-by-character animation, then click submit.
{
"$schema": "https://webreel.dev/schema/v1.json",
"videos": {
"form-filling": {
"url": "./web/index.html",
"viewport": { "width": 1920, "height": 1080 },
"zoom": 2,
"waitFor": "#email",
"defaultDelay": 300,
"steps": [
{ "action": "pause", "ms": 500 },
{
"action": "type",
"text": "user@example.com",
"selector": "#email",
"charDelay": 40
},
{
"action": "type",
"text": "supersecret123",
"selector": "#password",
"charDelay": 30,
"delay": 500
},
{ "action": "click", "text": "Sign In" },
{ "action": "pause", "ms": 2500 }
]
}
}
}Drag and drop
Move items between containers on a kanban board.
{
"$schema": "https://webreel.dev/schema/v1.json",
"videos": {
"drag-and-drop": {
"url": "./web/index.html",
"viewport": { "width": 1920, "height": 1080 },
"zoom": 2,
"waitFor": ".board",
"steps": [
{ "action": "pause", "ms": 500 },
{ "action": "moveTo", "text": "Write unit tests", "delay": 400 },
{
"action": "drag",
"from": { "text": "Write unit tests", "within": ".column-todo" },
"to": { "selector": ".card-list", "within": ".column-in-progress" },
"delay": 600
},
{
"action": "drag",
"from": { "text": "Build API endpoints", "within": ".column-in-progress" },
"to": { "selector": ".card-list", "within": ".column-done" },
"delay": 1000
}
]
}
}
}Keyboard shortcuts
Demonstrate key combos in a text editor.
{
"$schema": "https://webreel.dev/schema/v1.json",
"videos": {
"keyboard-shortcuts": {
"url": "./web/index.html",
"viewport": { "width": 1920, "height": 1080 },
"zoom": 2,
"waitFor": ".editor",
"defaultDelay": 600,
"steps": [
{ "action": "pause", "ms": 500 },
{ "action": "click", "selector": ".editor" },
{ "action": "key", "key": "cmd+a" },
{ "action": "key", "key": "cmd+b", "delay": 800 },
{ "action": "click", "selector": ".editor p:first-of-type" },
{ "action": "key", "key": "cmd+s", "delay": 1200 },
{ "action": "key", "key": "cmd+k", "delay": 1000 },
{ "action": "key", "key": "Escape", "delay": 800 },
{ "action": "pause", "ms": 500 }
]
}
}
}Custom theme
Override the cursor image and keystroke HUD appearance.
{
"$schema": "https://webreel.dev/schema/v1.json",
"theme": {
"cursor": {
"image": "./cursor.svg",
"size": 32,
"hotspot": "center"
},
"hud": {
"background": "rgba(30, 41, 59, 0.85)",
"color": "#e2e8f0",
"fontSize": 48,
"fontFamily": "\"SF Mono\", \"Fira Code\", monospace",
"borderRadius": 12,
"position": "top"
}
},
"videos": {
"custom-theme": {
"url": "./web/index.html",
"viewport": { "width": 1920, "height": 1080 },
"zoom": 2,
"waitFor": ".editor",
"defaultDelay": 400,
"steps": [
{ "action": "pause", "ms": 500 },
{ "action": "click", "text": "config.ts" },
{ "action": "click", "text": "utils.ts", "delay": 800 },
{ "action": "key", "key": "cmd+s", "delay": 1000 },
{ "action": "pause", "ms": 1500 }
]
}
}
}Multiple videos in one config
Define several recordings that share top-level settings.
{
"$schema": "https://webreel.dev/schema/v1.json",
"viewport": { "width": 1920, "height": 1080 },
"defaultDelay": 500,
"videos": {
"homepage": {
"url": "./web/index.html",
"zoom": 2,
"waitFor": ".hero",
"steps": [
{ "action": "pause", "ms": 500 },
{ "action": "click", "text": "Get Started", "delay": 1000 }
]
},
"features": {
"url": "./web/index.html",
"zoom": 2,
"waitFor": ".features",
"steps": [
{ "action": "pause", "ms": 500 },
{ "action": "scroll", "y": 400, "delay": 600 },
{ "action": "click", "text": "Fast Deploys", "delay": 1000 }
]
},
"pricing": {
"url": "./web/index.html",
"zoom": 2,
"waitFor": ".pricing",
"steps": [
{ "action": "pause", "ms": 500 },
{ "action": "scroll", "y": 600 },
{ "action": "click", "text": "Subscribe", "delay": 1000 }
]
}
}
}Record a specific subset: webreel record homepage pricing.
Shared steps via include
Factor reusable steps into a separate file and include them.
steps/setup.json:
{
"steps": [
{ "action": "wait", "selector": ".cookie-banner", "timeout": 5000 },
{ "action": "click", "selector": ".accept-btn", "delay": 300 }
]
}webreel.config.json:
{
"$schema": "https://webreel.dev/schema/v1.json",
"include": ["./steps/setup.json"],
"videos": {
"shared-steps": {
"url": "./web/index.html",
"viewport": { "width": 1920, "height": 1080 },
"zoom": 2,
"waitFor": ".page",
"steps": [
{ "action": "click", "text": "Learn more about our platform", "delay": 1000 }
]
}
}
}The included steps run before the video's own steps.
Mobile viewport
Record a mobile-sized viewport with a centered cursor hotspot.
{
"$schema": "https://webreel.dev/schema/v1.json",
"videos": {
"mobile-viewport": {
"url": "./web/index.html",
"viewport": { "width": 390, "height": 844 },
"zoom": 2,
"waitFor": ".content",
"theme": {
"cursor": { "hotspot": "center" }
},
"defaultDelay": 600,
"steps": [
{ "action": "pause", "ms": 500 },
{ "action": "click", "selector": ".menu-btn" },
{ "action": "pause", "ms": 1000 },
{ "action": "click", "selector": "#menuOverlay" },
{ "action": "scroll", "y": 300, "delay": 800 },
{ "action": "pause", "ms": 500 }
]
}
}
}GIF output
Set the output field to a .gif extension.
{
"$schema": "https://webreel.dev/schema/v1.json",
"videos": {
"gif-output": {
"url": "./web/index.html",
"viewport": { "width": 1920, "height": 1080 },
"zoom": 2,
"output": "gif-output.gif",
"waitFor": ".primary",
"steps": [
{ "action": "pause", "ms": 500 },
{ "action": "click", "selector": "a.primary", "delay": 1000 }
]
}
}
}WebM output
Set the output field to a .webm extension.
{
"$schema": "https://webreel.dev/schema/v1.json",
"videos": {
"webm-output": {
"url": "./web/index.html",
"viewport": { "width": 1920, "height": 1080 },
"zoom": 2,
"output": "webm-output.webm",
"waitFor": ".chart",
"steps": [
{ "action": "pause", "ms": 500 },
{ "action": "click", "text": "View All Reports", "delay": 1000 }
]
}
}
}Step types reference
Every step object requires an action field. Most steps also accept these optional fields:
label(string) - display label for the keystroke HUD overlaydelay(number) - milliseconds to wait after the step completesdescription(string) - human-readable note (not used at runtime)
Element targeting
Several steps target DOM elements via these fields:
text(string) - match by visible text contentselector(string) - match by CSS selectorwithin(string) - CSS selector to scope the search to a parent element
Use text or selector, not both. within is always optional.
---
pause
Wait for a fixed duration.
| Field | Type | Required | Description |
|---|---|---|---|
action | "pause" | yes | |
ms | number | yes | Duration in milliseconds |
{ "action": "pause", "ms": 500 }click
Click a DOM element.
| Field | Type | Required | Description |
|---|---|---|---|
action | "click" | yes | |
text | string | no | Match element by visible text |
selector | string | no | Match element by CSS selector |
within | string | no | Scope search to a parent selector |
modifiers | string[] | no | Modifier keys held during click (e.g. ["shift"], ["cmd"]) |
Provide text or selector (at least one).
{ "action": "click", "text": "Submit" }
{ "action": "click", "selector": "#save-btn", "delay": 500 }
{ "action": "click", "text": "Item 3", "modifiers": ["shift"] }
{ "action": "click", "text": "Delete", "within": ".modal" }type
Type text into an input element. If no target is specified, types into the currently focused element.
| Field | Type | Required | Description |
|---|---|---|---|
action | "type" | yes | |
text | string | yes | Text to type |
selector | string | no | Target input by CSS selector |
within | string | no | Scope search to a parent selector |
charDelay | number | no | Milliseconds between each character |
{ "action": "type", "text": "user@example.com", "selector": "#email", "charDelay": 40 }key
Press a keyboard shortcut or key combination.
| Field | Type | Required | Description |
|---|---|---|---|
action | "key" | yes | |
key | string | yes | Key or combo string |
target | string or ElementTarget | no | Element to focus before pressing |
Key combo syntax uses + to join modifiers: "cmd+s", "ctrl+shift+p", "alt+tab", "Enter", "Escape", "ArrowDown".
Modifier names: cmd, ctrl, shift, alt, meta.
{ "action": "key", "key": "cmd+s" }
{ "action": "key", "key": "Enter", "delay": 500 }
{ "action": "key", "key": "ctrl+shift+p", "label": "Command Palette" }drag
Drag from one element to another.
| Field | Type | Required | Description |
|---|---|---|---|
action | "drag" | yes | |
from | ElementTarget | yes | Source element ({ text?, selector?, within? }) |
to | ElementTarget | yes | Destination element ({ text?, selector?, within? }) |
{
"action": "drag",
"from": { "text": "Task A", "within": ".column-todo" },
"to": { "selector": ".card-list", "within": ".column-done" },
"delay": 600
}scroll
Scroll the page or a specific element.
| Field | Type | Required | Description |
|---|---|---|---|
action | "scroll" | yes | |
x | number | no | Horizontal scroll delta in pixels |
y | number | no | Vertical scroll delta in pixels |
text | string | no | Scroll element matched by text |
selector | string | no | Scroll element matched by CSS selector |
within | string | no | Scope search to a parent selector |
If no element target is given, scrolls the page.
{ "action": "scroll", "y": 400 }
{ "action": "scroll", "y": 300, "selector": ".scrollable-panel" }wait
Wait for an element to appear in the DOM.
| Field | Type | Required | Description |
|---|---|---|---|
action | "wait" | yes | |
selector | string | no | Wait for element matching CSS selector |
text | string | no | Wait for element containing text |
within | string | no | Scope search to a parent selector |
timeout | number | no | Maximum wait time in ms (default varies) |
Provide selector or text (at least one).
{ "action": "wait", "selector": ".results-loaded", "timeout": 5000 }
{ "action": "wait", "text": "Success" }moveTo
Move the cursor to an element without clicking.
| Field | Type | Required | Description |
|---|---|---|---|
action | "moveTo" | yes | |
text | string | no | Match element by visible text |
selector | string | no | Match element by CSS selector |
within | string | no | Scope search to a parent selector |
{ "action": "moveTo", "text": "Settings", "delay": 400 }hover
Hover over an element (triggers CSS :hover and mouseenter events).
| Field | Type | Required | Description |
|---|---|---|---|
action | "hover" | yes | |
text | string | no | Match element by visible text |
selector | string | no | Match element by CSS selector |
within | string | no | Scope search to a parent selector |
{ "action": "hover", "selector": ".tooltip-trigger" }navigate
Navigate the browser to a new URL.
| Field | Type | Required | Description |
|---|---|---|---|
action | "navigate" | yes | |
url | string | yes | URL to navigate to (absolute or relative to baseUrl) |
{ "action": "navigate", "url": "https://example.com/dashboard" }
{ "action": "navigate", "url": "/settings" }select
Select a value in a <select> dropdown.
| Field | Type | Required | Description |
|---|---|---|---|
action | "select" | yes | |
text | string | no | Match select element by visible text |
selector | string | no | Match select element by CSS selector |
within | string | no | Scope search to a parent selector |
value | string | yes | The option value to select |
{ "action": "select", "selector": "#country", "value": "us" }screenshot
Capture a PNG screenshot of the current viewport.
| Field | Type | Required | Description |
|---|---|---|---|
action | "screenshot" | yes | |
output | string | yes | File path for the PNG output |
{ "action": "screenshot", "output": "screenshots/final-state.png" }Related skills
FAQ
What output formats are supported?
MP4, GIF, and WebM recordings from JSON configuration.
How are interactions defined?
JSON config specifies browser actions, cursor paths, keystrokes, and sound effects.
Can I add sound effects?
Yes. Sound effects synchronize with scripted browser interactions.
Is Webreel safe to install?
skills.sh reports 1 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.