playwright-recording

Playwright Video Recording

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "playwright-recording" with this command: npx skills add digitalsamba/claude-code-video-toolkit/digitalsamba-claude-code-video-toolkit-playwright-recording

Playwright Video Recording

Playwright can record browser interactions as video - perfect for demo footage in Remotion compositions.

Quick Start

Installation

In your video project

npm init -y npm install -D playwright @playwright/test npx playwright install chromium

Basic Recording Script

// scripts/record-demo.ts import { chromium } from 'playwright';

async function recordDemo() { const browser = await chromium.launch(); const context = await browser.newContext({ viewport: { width: 1920, height: 1080 }, recordVideo: { dir: './recordings', size: { width: 1920, height: 1080 } } });

const page = await context.newPage();

// Your recording actions await page.goto('https://example.com'); await page.waitForTimeout(2000); await page.click('button.demo'); await page.waitForTimeout(3000);

// Close to save video await context.close(); await browser.close();

console.log('Recording saved to ./recordings/'); }

recordDemo();

Run with:

npx ts-node scripts/record-demo.ts

or

npx tsx scripts/record-demo.ts

Recording Configuration

Viewport Sizes

// Standard 1080p (recommended for Remotion) viewport: { width: 1920, height: 1080 }

// 720p (smaller files) viewport: { width: 1280, height: 720 }

// Square (social media) viewport: { width: 1080, height: 1080 }

// Mobile viewport: { width: 390, height: 844 } // iPhone 14

Video Quality Settings

const context = await browser.newContext({ viewport: { width: 1920, height: 1080 }, recordVideo: { dir: './recordings', size: { width: 1920, height: 1080 } // Match viewport for crisp output }, // Slow down for visibility // Note: slowMo is on browser launch, not context });

// For slow motion, launch browser with slowMo const browser = await chromium.launch({ slowMo: 100 // 100ms delay between actions });

Recording Patterns

Form Submission Demo

import { chromium } from 'playwright';

async function recordFormDemo() { const browser = await chromium.launch({ slowMo: 50 }); const context = await browser.newContext({ viewport: { width: 1920, height: 1080 }, recordVideo: { dir: './recordings', size: { width: 1920, height: 1080 } } }); const page = await context.newPage();

await page.goto('https://myapp.com/form'); await page.waitForTimeout(1000);

// Type with realistic speed await page.fill('#name', 'John Smith', { timeout: 5000 }); await page.waitForTimeout(500);

await page.fill('#email', 'john@example.com'); await page.waitForTimeout(500);

// Click submit await page.click('button[type="submit"]');

// Wait for result await page.waitForSelector('.success-message'); await page.waitForTimeout(2000);

await context.close(); await browser.close(); }

Multi-Page Navigation

async function recordNavDemo() { const browser = await chromium.launch({ slowMo: 100 }); const context = await browser.newContext({ viewport: { width: 1920, height: 1080 }, recordVideo: { dir: './recordings', size: { width: 1920, height: 1080 } } }); const page = await context.newPage();

// Page 1 await page.goto('https://myapp.com'); await page.waitForTimeout(2000);

// Navigate to page 2 await page.click('nav a[href="/features"]'); await page.waitForLoadState('networkidle'); await page.waitForTimeout(2000);

// Navigate to page 3 await page.click('nav a[href="/pricing"]'); await page.waitForLoadState('networkidle'); await page.waitForTimeout(2000);

await context.close(); await browser.close(); }

Scroll Demo

async function recordScrollDemo() { const browser = await chromium.launch(); const context = await browser.newContext({ viewport: { width: 1920, height: 1080 }, recordVideo: { dir: './recordings', size: { width: 1920, height: 1080 } } }); const page = await context.newPage();

await page.goto('https://myapp.com/long-page'); await page.waitForTimeout(1000);

// Smooth scroll await page.evaluate(async () => { const delay = (ms: number) => new Promise(r => setTimeout(r, ms)); for (let i = 0; i < 10; i++) { window.scrollBy({ top: 200, behavior: 'smooth' }); await delay(300); } });

await page.waitForTimeout(1000); await context.close(); await browser.close(); }

Login Flow

async function recordLoginDemo() { const browser = await chromium.launch({ slowMo: 75 }); const context = await browser.newContext({ viewport: { width: 1920, height: 1080 }, recordVideo: { dir: './recordings', size: { width: 1920, height: 1080 } } }); const page = await context.newPage();

await page.goto('https://myapp.com/login'); await page.waitForTimeout(1000);

await page.fill('#email', 'demo@example.com'); await page.waitForTimeout(300);

await page.fill('#password', '••••••••'); await page.waitForTimeout(500);

await page.click('button[type="submit"]');

// Wait for dashboard await page.waitForURL('**/dashboard'); await page.waitForTimeout(3000);

await context.close(); await browser.close(); }

Cursor Highlighting

Playwright doesn't show cursor by default. Add visual indicators:

CSS Cursor Highlight

// Inject cursor visualization await page.addStyleTag({ content: * { cursor: none !important; } .playwright-cursor { position: fixed; width: 24px; height: 24px; background: rgba(255, 100, 100, 0.5); border: 2px solid rgba(255, 50, 50, 0.8); border-radius: 50%; pointer-events: none; z-index: 999999; transform: translate(-50%, -50%); transition: transform 0.1s ease; } .playwright-cursor.clicking { transform: translate(-50%, -50%) scale(0.8); background: rgba(255, 50, 50, 0.8); } });

// Add cursor element await page.evaluate(() => { const cursor = document.createElement('div'); cursor.className = 'playwright-cursor'; document.body.appendChild(cursor);

document.addEventListener('mousemove', (e) => { cursor.style.left = e.clientX + 'px'; cursor.style.top = e.clientY + 'px'; });

document.addEventListener('mousedown', () => cursor.classList.add('clicking')); document.addEventListener('mouseup', () => cursor.classList.remove('clicking')); });

Click Ripple Effect

// Add click ripple visualization await page.addStyleTag({ content: .click-ripple { position: fixed; width: 40px; height: 40px; border-radius: 50%; background: rgba(234, 88, 12, 0.4); pointer-events: none; z-index: 999998; transform: translate(-50%, -50%) scale(0); animation: ripple 0.4s ease-out forwards; } @keyframes ripple { to { transform: translate(-50%, -50%) scale(2); opacity: 0; } } });

// Custom click function with ripple async function clickWithRipple(page, selector) { const element = await page.locator(selector); const box = await element.boundingBox();

await page.evaluate(({ x, y }) => { const ripple = document.createElement('div'); ripple.className = 'click-ripple'; ripple.style.left = x + 'px'; ripple.style.top = y + 'px'; document.body.appendChild(ripple); setTimeout(() => ripple.remove(), 400); }, { x: box.x + box.width / 2, y: box.y + box.height / 2 });

await element.click(); }

Output for Remotion

Move Recording to public/demos/

import { chromium } from 'playwright'; import * as fs from 'fs'; import * as path from 'path';

async function recordForRemotion(outputName: string) { const browser = await chromium.launch({ slowMo: 50 }); const context = await browser.newContext({ viewport: { width: 1920, height: 1080 }, recordVideo: { dir: './temp-recordings', size: { width: 1920, height: 1080 } } }); const page = await context.newPage();

// ... recording actions ...

await context.close();

// Get the video path const video = page.video(); const videoPath = await video?.path();

if (videoPath) { const destPath = ./public/demos/${outputName}.webm; fs.mkdirSync(path.dirname(destPath), { recursive: true }); fs.renameSync(videoPath, destPath); console.log(Recording saved to: ${destPath});

// Get duration for config
// Use ffprobe: ffprobe -v error -show_entries format=duration -of csv=p=0 file.webm

}

await browser.close(); }

Convert WebM to MP4

Playwright outputs WebM. Convert for better Remotion compatibility:

ffmpeg -i recording.webm -c:v libx264 -crf 20 -preset medium -movflags faststart public/demos/demo.mp4

Interactive Recording

For user-driven recordings where you manually perform actions:

// Inject ESC key listener to stop recording async function injectStopListener(page: Page): Promise<void> { await page.evaluate(() => { if ((window as any).__escListenerAdded) return; (window as any).__escListenerAdded = true; (window as any).__stopRecording = false; document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { e.preventDefault(); (window as any).__stopRecording = true; } }); }); }

// Poll for stop signal - handle navigation errors gracefully while (!stopped) { try { const shouldStop = await page.evaluate(() => (window as any).__stopRecording === true); if (shouldStop) break; } catch { // Page navigating - continue recording } await new Promise(r => setTimeout(r, 200)); }

Key insight: page.evaluate() throws during navigation. Use try/catch and continue - don't treat errors as stop signals.

Window Scaling for Laptops

Record at full 1080p while showing a smaller window:

const scale = 0.75; // 75% window size const context = await browser.newContext({ viewport: { width: 1920 * scale, height: 1080 * scale }, deviceScaleFactor: 1 / scale, recordVideo: { dir: './recordings', size: { width: 1920, height: 1080 } }, });

Cookie Banner Dismissal

Comprehensive selector list for common consent platforms:

const COOKIE_SELECTORS = [ '#onetrust-accept-btn-handler', // OneTrust '#CybotCookiebotDialogBodyButtonAccept', // Cookiebot '.cc-btn.cc-dismiss', // Cookie Consent by Insites '[class*="cookie"] button[class*="accept"]', '[class*="consent"] button[class*="accept"]', 'button:has-text("Accept all")', 'button:has-text("Accept cookies")', 'button:has-text("Got it")', ];

async function dismissCookieBanners(page: Page): Promise<void> { await page.waitForTimeout(500); for (const selector of COOKIE_SELECTORS) { try { const btn = page.locator(selector).first(); if (await btn.isVisible({ timeout: 100 })) { await btn.click({ timeout: 500 }); return; } } catch { /* try next */ } } }

Call after page.goto() and on page.on('load') for navigation.

Important: Injected Elements Appear in Video

Warning: Any DOM elements you inject (cursors, control panels, overlays) will be recorded. For UI-free recordings, use terminal-based controls only (Ctrl+C, max duration timer).

Tips for Good Demo Recordings

  • Use slowMo - 50-100ms makes actions visible

  • Add waitForTimeout - Pause between actions for comprehension

  • Wait for animations - Use waitForLoadState('networkidle')

  • Match Remotion dimensions - 1920x1080 at 30fps typical

  • Test without recording first - Debug before final capture

  • Clear browser state - Use fresh context for clean demos

  • Dismiss cookie banners - Use comprehensive selector list above

  • Re-inject on navigation - Cursor/listeners reset on page load

Feedback & Contributions

If this skill is missing information or could be improved:

  • Missing a pattern? Describe what you needed

  • Found an error? Let me know what's wrong

  • Want to contribute? I can help you:

  • Update this skill with improvements

  • Create a PR to github.com/digitalsamba/claude-code-video-toolkit

Just say "improve this skill" and I'll guide you through updating .claude/skills/playwright-recording/SKILL.md .

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

ffmpeg

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

elevenlabs

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

remotion

No summary provided by upstream source.

Repository SourceNeeds Review