
About
Record polished UI demo videos using Playwright. Use when the user asks to create a demo, walkthrough, screen recording, or tutorial video of a web application. Produces WebM videos with visible cursor, natural pacing, and professional feel.
name: ui-demo description: Record polished UI demo videos using Playwright. Use when the user asks to create a demo, walkthrough, screen recording, or tutorial video of a web application. Produces WebM videos with visible cursor, natural pacing, and professional feel. origin: ECC
UI Demo Video Recorder
Record polished demo videos of web applications using Playwright's video recording with an injected cursor overlay, natural pacing, and storytelling flow.
When to Use
- User asks for a "demo video", "screen recording", "walkthrough", or "tutorial"
- User wants to showcase a feature or workflow visually
- User needs a video for documentation, onboarding, or stakeholder presentation
Three-Phase Process
Every demo goes through three phases: Discover -> Rehearse -> Record. Never skip straight to recording.
Phase 1: Discover
Before writing any script, explore the target pages to understand what is actually there.
Why
You cannot script what you have not seen. Fields may be <input> not <textarea>, dropdowns may be custom components not <select>, and comment boxes may support @mentions or #tags. Assumptions break recordings silently.
How
Navigate to each page in the flow and dump its interactive elements:
// Run this for each page in the flow BEFORE writing the demo script
const fields = await page.evaluate(() => {
const els = [];
document.querySelectorAll('input, select, textarea, button, [contenteditable]').forEach(el => {
if (el.offsetParent !== null) {
els.push({
tag: el.tagName,
type: el.type || '',
name: el.name || '',
placeholder: el.placeholder || '',
text: el.textContent?.trim().substring(0, 40) || '',
contentEditable: el.contentEditable === 'true',
role: el.getAttribute('role') || '',
});
}
});
return els;
});
console.log(JSON.stringify(fields, null, 2));
What to look for
- Form fields: Are they
<select>,<input>, custom dropdowns, or comboboxes? - Select options: Dump option values AND text. Placeholders often have
value="0"orvalue=""which looks non-empty. UseArray.from(el.options).map(o => ({ value: o.value, text: o.text })). Skip options where text includes "Select" or value is"0". - Rich text: Does the comment box support
@mentions,#tags, markdown, or emoji? Check placeholder text. - Required fields: Which fields block form submission? Check
required,*in labels, and try submitting empty to see validation errors. - Dynamic content: Do fields appear after other fields are filled?
- Button labels: Exact text such as
"Submit","Submit Request", or"Send". - Table column headers: For table-driven modals, map each
input[type="number"]to its column header instead of assuming all numeric inputs mean the same thing.
Output
A field map for each page, used to write correct selectors in the script. Example:
/purchase-requests/new:
- Budget Code: <select> (first select on page, 4 options)
- Desired Delivery: <input type="date">
- Context: <textarea> (not input)
- BOM table: inline-editable cells with span.cursor-pointer -> input pattern
- Submit: <button> text="Submit"
/purchase-requests/N (detail):
- Comment: <input placeholder="Type a message..."> supports @user and #PR tags
- Send: <button> text="Send" (disabled until input has content)
Phase 2: Rehearse
Run through all steps without recording. Verify every selector resolves.
Why
Silent selector failures are the main reason demo recordings break. Rehearsal catches them before you waste a recording.
How
Use ensureVisible, a wrapper that logs and fails loudly:
async function ensureVisible(page, locator, label) {
const el = typeof locator === 'string' ? page.locator(locator).first() : locator;
const visible = await el.isVisible().catch(() => false);
if (!visible) {
const msg = `REHEARSAL FAIL: "${label}" not found - selector: ${typeof locator === 'string' ? locator : '(locator object)'}`;
console.error(msg);
const found = await page.evaluate(() => {
return Array.from(document.querySelectorAll('button, input, select, textarea, a'))
.filter(el => el.offsetParent !== null)
.map(el => `${el.tagName}[${el.type || ''}] "${el.textContent?.trim().substring(0, 30)}"`)
.join('\n ');
});
console.error(' Visible elements:\n ' + found);
return false;
}
console.log(`REHEARSAL OK: "${label}"`);
return true;
}
Rehearsal script structure
const steps = [
{ label: 'Login email field', selector: '#email' },
{ label: 'Login submit', selector: 'button[type="submit"]' },
{ label: 'New Request button', selector: 'button:has-text("New Request")' },
{ label: 'Budget Code select', selector: 'select' },
{ label: 'Delivery date', selector: 'input[type="date"]:visible' },
{ label: 'Description
