Create professional UI walkthroughs and demo videos automatically using Playwright.
Install
mkdir -p .claude/skills/ui-demo && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/12091" && unzip -o skill.zip -d .claude/skills/ui-demo && rm skill.zipInstalls to .claude/skills/ui-demo
Activation
This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.
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.Key capabilities
- →Explore target web pages to understand interactive elements.
- →Rehearse all steps of the demo without recording to verify selectors.
- →Record polished UI demo videos using Playwright's video recording.
- →Inject a visible cursor overlay for professional-looking tutorials.
- →Add subtitles at major transitions and use natural pacing for storytelling.
How it works
The skill follows a three-phase process of discovery, rehearsal, and recording to capture interactive web demos using Playwright, injecting a cursor overlay and subtitles.
Inputs & outputs
When to use ui-demo
- →Create feature walkthroughs
- →Generate video documentation
- →Record tutorials for stakeholders
About this skill
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 field', selector: 'textarea:visible' },
{ label: 'Add Item button', selector: 'button:has-text("Add Item")' },
{ label: 'Submit button', selector: 'button:has-text("Submit")' },
];
let allOk = true;
for (const step of steps) {
if (!await ensureVisible(page, step.selector, step.label)) {
allOk = false;
}
}
if (!allOk) {
console.error('REHEARSAL FAILED - fix selectors before recording');
process.exit(1);
}
console.log('REHEARSAL PASSED - all selectors verified');
When rehearsal fails
- Read the visible-element dump.
- Find the correct selector.
- Update the script.
- Re-run rehearsal.
- Only proceed when every selector passes.
Phase 3: Record
Only after discovery and rehearsal pass should you create the recording.
Recording Principles
1. Storytelling Flow
Plan the video as a story. Follow user-specified order, or use this default:
- Entry: Login or navigate to the starting point
- Context: Pan the surroundings so viewers orient themselves
- Action: Perform the main workflow steps
- Variation: Show a secondary feature such as settings, theme, or localization
- Result: Show the outcome, confirmation, or new state
2. Pacing
- After login:
4s - After navigation:
3s - After clicking a button:
2s - Between major steps:
1.5-2s - After the final action:
3s - Typing delay:
25-40msper character
3. Cursor Overlay
Inject an SVG arrow cursor that follows mouse movements:
async function injectCursor(page) {
await page.evaluate(() => {
if (document.getElementById('demo-cursor')) return;
const cursor = document.createElement('div');
cursor.id = 'demo-cursor';
cursor.innerHTML = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 3L19 12L12 13L9 20L5 3Z" fill="white" stroke="black" stroke-width="1.5" stroke-linejoin="round"/>
</svg>`;
cursor.style.cssText = `
position: fixed; z-index: 999999; pointer-events: none;
width: 24px; height: 24px;
transition: left 0.1s, top 0.1s;
filter: drop-shadow(1px 1px 2px rgba(0,0,0,0.3));
`;
cursor.style.left = '0px';
cursor.style.top = '0px';
document.body.appendChild(cursor);
document.addEventListener('mousemove', (e) => {
cursor.style.left = e.clientX + 'px';
cursor.style.top = e.clientY + 'px';
});
});
}
Call injectCursor(page) after every page navigation because the overlay is destroyed on navigate.
4. Mouse Movement
Never teleport the cursor. Move to the target before clicking:
async function moveAndClick(page, locator, label, opts = {}) {
const { postClickDelay = 800, ...clickOpts } = opts;
const el = typeof locator === 'string' ? page.locator(locator).first() : locator;
const visible = await el.isVisible().catch(() => false);
if (!visible) {
console.error(`WARNING: moveAndClick skipped - "${label}" not visible`);
return false;
}
try {
await el.scrollIntoViewIfNeeded();
await page.waitForTimeout(300);
const box = await el.boundingBox();
if (box) {
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2, { steps: 10 });
await page.waitForTimeout(400);
}
await el.click(clickOpts);
} catch (e) {
console.error(`WARNING: moveAndClick failed on "${label}": ${e.message}`);
return false;
}
await page.waitForTimeout(postClickDelay);
return true;
}
Every call should include a descriptive label for debugging.
5. Typing
Type visibly, not instant-fill:
async function typeSlowly(page, locator, text, label, charDelay = 35) {
const el = typeof locator === 'string' ? page.locator(locator).first() : locator;
const visible = await el.isVisible().catch(() => false);
if (!visible) {
console.error(`WARNING: typeSlowly skipped - "${label}" not visible`);
return false;
}
await moveAndClick(page, el, label);
await el.fill('');
await el.pressSequentially(text, { delay: charDelay });
await page.waitForTimeout(500);
return true;
}
6. Scrolling
Use smooth scroll instead of jumps:
await page.evaluate(() => window.scrollTo({ top: 400, behavior: 'smooth' }));
await page.waitForTimeout(1500);
7. Dashboard Panning
When showing a dashboard or overview page, move the cursor across key elements:
async function panElements(page, selector, maxCount = 6) {
const elements = await page.locator(selector).all();
for (let i = 0; i < Math.min(elements.length, maxCount); i++) {
try {
const box = await elements[i].boundingBox();
if (box && box.y < 700) {
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2, { steps: 8 });
await page.waitForTimeout(600);
}
} catch (e) {
console.warn(`WARNING: panElements skipped element ${i} (selector: "${selector}"): ${e.message}`);
}
}
}
8. Subtitles
Inject a subtitle bar at the bottom of the viewport:
async function injectSubtitleBar(page) {
await page.evaluate(() => {
if (document.getElementById('demo-subtitle')) return;
const bar = document.createElement('div');
bar.id = 'demo-subtitle';
---
*Content truncated.*
When not to use it
- →When the user wants to skip the discovery and rehearsal phases.
- →When the user wants to record a demo without a visible cursor overlay.
- →When the user wants to record a demo with silent selector failures.
Limitations
- →Requires a three-phase process: Discover -> Rehearse -> Record.
- →Requires Playwright for video recording.
- →Cursor and subtitle overlays need to be re-injected after every navigation.
How it compares
This skill provides a structured, verified process for creating high-quality UI demo videos with professional overlays and pacing, unlike simple screen recording tools.
Compared to similar skills
ui-demo side by side with the closest alternatives in the catalog.
| Skill | Installs | Updated | Safety | Difficulty |
|---|---|---|---|---|
| ui-demo (this skill) | 0 | 2mo | Review | Advanced |
| storybook | 0 | 4mo | Review | Intermediate |
| svelte-expert | 11 | 9mo | No flags | Intermediate |
| home-assistant-integration-knowledge | 8 | 2mo | No flags | Advanced |
Try saying
Example prompts that trigger this skill in your AI assistant.
You might also like
storybook
GhAli21
>
svelte-expert
Raudbjorn
Expert Svelte/SvelteKit development assistant for building components, utilities, and applications. Use when creating Svelte components, SvelteKit applications, implementing reactive patterns, handling state management, working with stores, transitions, animations, or any Svelte/SvelteKit development task. Includes comprehensive documentation access, code validation with svelte-autofixer, and playground link generation.
home-assistant-integration-knowledge
home-assistant
Everything you need to know to build, test and review Home Assistant Integrations. If you're looking at an integration, you must use this as your primary reference.
security-requirement-extraction
wshobson
Derive security requirements from threat models and business context. Use when translating threats into actionable requirements, creating security user stories, or building security test cases.
writing-skills
obra
Use when creating new skills, editing existing skills, or verifying skills work before deployment
obsidian-hello-world
jeremylongshore
Create a minimal working Obsidian plugin with commands and settings. Use when building your first plugin feature, testing your setup, or learning basic Obsidian plugin patterns. Trigger with phrases like "obsidian hello world", "first obsidian plugin", "obsidian quick start", "simple obsidian plugin".