--- id: digitalsamba/claude-code-video-toolkit/playwright-recording version: "3f528701" license: MIT install: manual updated: 2026-07-06 --- # playwright-recording — playwright-recording lets you automatically capture browser sessions and user interactions as video files, perfect for building demo content and technical documentation. Works seamlessly with Claude Code to record complex workflows and generate visual artifacts from your automation scripts. Publisher: digitalsamba · Stars: 1845 · Updated: 2026-07-06 Install (manual): `git clone https://github.com/digitalsamba/claude-code-video-toolkit` ## SKILL.md # Playwright Video Recording Playwright can record browser interactions as video - perfect for demo footage in Remotion compositions. ## Quick Start ### Installation ```bash # In your video project npm init -y npm install -D playwright @playwright/test npx playwright install chromium ``` ### Basic Recording Script ```typescript // 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: ```bash npx ts-node scripts/record-demo.ts # or npx tsx scripts/record-demo.ts ``` ## Recording Configuration ### Viewport Sizes ```typescript // 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 ```typescript 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 ```typescript 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 ```typescript 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 ```typescript 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 ```typescript 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 ```typescript // 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 ```typescript // 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/ ```typescript 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: ```bash 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: ```typescript // Inject ESC key listener to stop recording async function injectStopListener(page: Page): Promise { 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: ```typescript 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: ```typescript 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 { 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 1. **Use slowMo** - 50-100ms makes actions visible 2. **Add waitForTimeout** - Pause between actions for comprehension 3. **Wait for animations** - Use `waitForLoadState('networkidle')` 4. **Match Remotion dimensions** - 1920x1080 at 30fps typical 5. **Test without recording first** - Debug before final capture 6. **Clear browser state** - Use fresh context for clean demos 7. **Dismiss cookie banners** - Use comprehensive selector list above 8. **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: 1. Update this skill with improvements 2. 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`. [View on SkillFed](https://skillfed.io/digitalsamba/claude-code-video-toolkit/playwright-recording) · [View on GitHub](https://github.com/digitalsamba/claude-code-video-toolkit)