playwright-recording · diff
git:20251210.0d96c3b to git:20251210.111550f
85 added, 2 removed. Audit A to A.
---
name: playwright-recording
description: Record browser interactions as video using Playwright. Use for capturing demo videos, app walkthroughs, and UI flows for Remotion videos. Triggers include recording a demo, capturing browser video, screen recording a website, or creating walkthrough footage.
---
# 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<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:
+
+ ```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<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
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. **Hide cookie banners** - Dismiss or inject CSS to hide
- 8. **Use consistent data** - Pre-fill forms with realistic placeholder data
+ 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`.