New: Earn 20% as an AgentAlley affiliate. Share your link and earn on every sale you refer. Join now New: Earn 20% as an affiliate. Join now

Record browser interactions as video using Playwright

Playwright Video Recording

by digitalsamba · MIT license · ★ 2,114 on GitHub · updated Sep 21

Playwright Video Recording cover

Record browser interactions as video using Playwright. Use for capturing demo videos, app walkthroughs, and UI flows for Remotion videos.

---
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

Overview

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

Manual install (npx, ZIP)
Claude Codenpx skills add digitalsamba/claude-code-video-toolkit --skill playwright-recording -g -a claude-code
Claude Code (manual)mkdir -p ~/.claude/skills && curl -sL https://codeload.github.com/digitalsamba/claude-code-video-toolkit/tar.gz/225c80a1c3a5 \ | tar -xz -C ~/.claude/skills --strip-components=3 claude-code-video-toolkit-225c80a1c3a5/.claude/skills/playwright-recording
Windows (PowerShell)New-Item -ItemType Directory -Force "$HOME\.claude\skills" | Out-Null; Invoke-WebRequest "https://m.agentalley.io/zip/0aaef63d919f7ce68d8d/playwright-recording.zip" -OutFile "$env:TEMP\playwright-recording.zip"; Expand-Archive "$env:TEMP\playwright-recording.zip" -DestinationPath "$HOME\.claude\skills" -Force
Claude.ai / desktopDownload the ZIP and upload it under Settings → Capabilities → Skills.
Codex, Cursor, other agentsPoint them at SKILL.md. The other files are plain text.

Connects with Agent Alley MCP · agentalley.io/mcp

Files included

  • SKILL.md12.4 KB
  • reference.md8.0 KB
Read the files here
digitalsamba/225c80a2 files
Shared references1SKILL.md reads these from elsewhere in the repo. Not in the ZIP.Get them on GitHub ↗
SKILL.md
Show the full text470 lines

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', '[email protected]');
  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', '[email protected]');
  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 } },
});

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

  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.

1---
2name: playwright-recording
3description: 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.
4---
5 
6# Playwright Video Recording
7 
8Playwright can record browser interactions as video - perfect for demo footage in Remotion compositions.
9 
10## Quick Start
11 
12### Installation
13 
14```bash
15# In your video project
16npm init -y
17npm install -D playwright @playwright/test
18npx playwright install chromium
19```
20 
21### Basic Recording Script
22 
23```typescript
24// scripts/record-demo.ts
25import { chromium } from 'playwright';
26 
27async function recordDemo() {
28 const browser = await chromium.launch();
29 const context = await browser.newContext({
30 viewport: { width: 1920, height: 1080 },
31 recordVideo: {
32 dir: './recordings',
33 size: { width: 1920, height: 1080 }
34 }
35 });
36 
37 const page = await context.newPage();
38 
39 // Your recording actions
40 await page.goto('https://example.com');
41 await page.waitForTimeout(2000);
42 await page.click('button.demo');
43 await page.waitForTimeout(3000);
44 
45 // Close to save video
46 await context.close();
47 await browser.close();
48 
49 console.log('Recording saved to ./recordings/');
50}
51 
52recordDemo();
53```
54 
55Run with:
56```bash
57npx ts-node scripts/record-demo.ts
58# or
59npx tsx scripts/record-demo.ts
60```
61 
62## Recording Configuration
63 
64### Viewport Sizes
65 
66```typescript
67// Standard 1080p (recommended for Remotion)
68viewport: { width: 1920, height: 1080 }
69 
70// 720p (smaller files)
71viewport: { width: 1280, height: 720 }
72 
73// Square (social media)
74viewport: { width: 1080, height: 1080 }
75 
76// Mobile
77viewport: { width: 390, height: 844 } // iPhone 14
78```
79 
80### Video Quality Settings
81 
82```typescript
83const context = await browser.newContext({
84 viewport: { width: 1920, height: 1080 },
85 recordVideo: {
86 dir: './recordings',
87 size: { width: 1920, height: 1080 } // Match viewport for crisp output
88 },
89 // Slow down for visibility
90 // Note: slowMo is on browser launch, not context
91});
92 
93// For slow motion, launch browser with slowMo
94const browser = await chromium.launch({
95 slowMo: 100 // 100ms delay between actions
96});
97```
98 
99## Recording Patterns
100 
101### Form Submission Demo
102 
103```typescript
104import { chromium } from 'playwright';
105 
106async function recordFormDemo() {
107 const browser = await chromium.launch({ slowMo: 50 });
108 const context = await browser.newContext({
109 viewport: { width: 1920, height: 1080 },
110 recordVideo: { dir: './recordings', size: { width: 1920, height: 1080 } }
111 });
112 const page = await context.newPage();
113 
114 await page.goto('https://myapp.com/form');
115 await page.waitForTimeout(1000);
116 
117 // Type with realistic speed
118 await page.fill('#name', 'John Smith', { timeout: 5000 });
119 await page.waitForTimeout(500);
120 
121 await page.fill('#email', '[email protected]');
122 await page.waitForTimeout(500);
123 
124 // Click submit
125 await page.click('button[type="submit"]');
126 
127 // Wait for result
128 await page.waitForSelector('.success-message');
129 await page.waitForTimeout(2000);
130 
131 await context.close();
132 await browser.close();
133}
134```
135 
136### Multi-Page Navigation
137 
138```typescript
139async function recordNavDemo() {
140 const browser = await chromium.launch({ slowMo: 100 });
141 const context = await browser.newContext({
142 viewport: { width: 1920, height: 1080 },
143 recordVideo: { dir: './recordings', size: { width: 1920, height: 1080 } }
144 });
145 const page = await context.newPage();
146 
147 // Page 1
148 await page.goto('https://myapp.com');
149 await page.waitForTimeout(2000);
150 
151 // Navigate to page 2
152 await page.click('nav a[href="/features"]');
153 await page.waitForLoadState('networkidle');
154 await page.waitForTimeout(2000);
155 
156 // Navigate to page 3
157 await page.click('nav a[href="/pricing"]');
158 await page.waitForLoadState('networkidle');
159 await page.waitForTimeout(2000);
160 
161 await context.close();
162 await browser.close();
163}
164```
165 
166### Scroll Demo
167 
168```typescript
169async function recordScrollDemo() {
170 const browser = await chromium.launch();
171 const context = await browser.newContext({
172 viewport: { width: 1920, height: 1080 },
173 recordVideo: { dir: './recordings', size: { width: 1920, height: 1080 } }
174 });
175 const page = await context.newPage();
176 
177 await page.goto('https://myapp.com/long-page');
178 await page.waitForTimeout(1000);
179 
180 // Smooth scroll
181 await page.evaluate(async () => {
182 const delay = (ms: number) => new Promise(r => setTimeout(r, ms));
183 for (let i = 0; i < 10; i++) {
184 window.scrollBy({ top: 200, behavior: 'smooth' });
185 await delay(300);
186 }
187 });
188 
189 await page.waitForTimeout(1000);
190 await context.close();
191 await browser.close();
192}
193```
194 
195### Login Flow
196 
197```typescript
198async function recordLoginDemo() {
199 const browser = await chromium.launch({ slowMo: 75 });
200 const context = await browser.newContext({
201 viewport: { width: 1920, height: 1080 },
202 recordVideo: { dir: './recordings', size: { width: 1920, height: 1080 } }
203 });
204 const page = await context.newPage();
205 
206 await page.goto('https://myapp.com/login');
207 await page.waitForTimeout(1000);
208 
209 await page.fill('#email', '[email protected]');
210 await page.waitForTimeout(300);
211 
212 await page.fill('#password', '••••••••');
213 await page.waitForTimeout(500);
214 
215 await page.click('button[type="submit"]');
216 
217 // Wait for dashboard
218 await page.waitForURL('**/dashboard');
219 await page.waitForTimeout(3000);
220 
221 await context.close();
222 await browser.close();
223}
224```
225 
226## Cursor Highlighting
227 
228Playwright doesn't show cursor by default. Add visual indicators:
229 
230### CSS Cursor Highlight
231 
232```typescript
233// Inject cursor visualization
234await page.addStyleTag({
235 content: `
236 * { cursor: none !important; }
237 .playwright-cursor {
238 position: fixed;
239 width: 24px;
240 height: 24px;
241 background: rgba(255, 100, 100, 0.5);
242 border: 2px solid rgba(255, 50, 50, 0.8);
243 border-radius: 50%;
244 pointer-events: none;
245 z-index: 999999;
246 transform: translate(-50%, -50%);
247 transition: transform 0.1s ease;
248 }
249 .playwright-cursor.clicking {
250 transform: translate(-50%, -50%) scale(0.8);
251 background: rgba(255, 50, 50, 0.8);
252 }
253 `
254});
255 
256// Add cursor element
257await page.evaluate(() => {
258 const cursor = document.createElement('div');
259 cursor.className = 'playwright-cursor';
260 document.body.appendChild(cursor);
261 
262 document.addEventListener('mousemove', (e) => {
263 cursor.style.left = e.clientX + 'px';
264 cursor.style.top = e.clientY + 'px';
265 });
266 
267 document.addEventListener('mousedown', () => cursor.classList.add('clicking'));
268 document.addEventListener('mouseup', () => cursor.classList.remove('clicking'));
269});
270```
271 
272### Click Ripple Effect
273 
274```typescript
275// Add click ripple visualization
276await page.addStyleTag({
277 content: `
278 .click-ripple {
279 position: fixed;
280 width: 40px;
281 height: 40px;
282 border-radius: 50%;
283 background: rgba(234, 88, 12, 0.4);
284 pointer-events: none;
285 z-index: 999998;
286 transform: translate(-50%, -50%) scale(0);
287 animation: ripple 0.4s ease-out forwards;
288 }
289 @keyframes ripple {
290 to {
291 transform: translate(-50%, -50%) scale(2);
292 opacity: 0;
293 }
294 }
295 `
296});
297 
298// Custom click function with ripple
299async function clickWithRipple(page, selector) {
300 const element = await page.locator(selector);
301 const box = await element.boundingBox();
302 
303 await page.evaluate(({ x, y }) => {
304 const ripple = document.createElement('div');
305 ripple.className = 'click-ripple';
306 ripple.style.left = x + 'px';
307 ripple.style.top = y + 'px';
308 document.body.appendChild(ripple);
309 setTimeout(() => ripple.remove(), 400);
310 }, { x: box.x + box.width / 2, y: box.y + box.height / 2 });
311 
312 await element.click();
313}
314```
315 
316## Output for Remotion
317 
318### Move Recording to public/demos/
319 
320```typescript
321import { chromium } from 'playwright';
322import * as fs from 'fs';
323import * as path from 'path';
324 
325async function recordForRemotion(outputName: string) {
326 const browser = await chromium.launch({ slowMo: 50 });
327 const context = await browser.newContext({
328 viewport: { width: 1920, height: 1080 },
329 recordVideo: { dir: './temp-recordings', size: { width: 1920, height: 1080 } }
330 });
331 const page = await context.newPage();
332 
333 // ... recording actions ...
334 
335 await context.close();
336 
337 // Get the video path
338 const video = page.video();
339 const videoPath = await video?.path();
340 
341 if (videoPath) {
342 const destPath = `./public/demos/${outputName}.webm`;
343 fs.mkdirSync(path.dirname(destPath), { recursive: true });
344 fs.renameSync(videoPath, destPath);
345 console.log(`Recording saved to: ${destPath}`);
346 
347 // Get duration for config
348 // Use ffprobe: ffprobe -v error -show_entries format=duration -of csv=p=0 file.webm
349 }
350 
351 await browser.close();
352}
353```
354 
355### Convert WebM to MP4
356 
357Playwright outputs WebM. Convert for better Remotion compatibility:
358 
359```bash
360ffmpeg -i recording.webm -c:v libx264 -crf 20 -preset medium -movflags faststart public/demos/demo.mp4
361```
362 
363## Interactive Recording
364 
365For user-driven recordings where you manually perform actions:
366 
367```typescript
368// Inject ESC key listener to stop recording
369async function injectStopListener(page: Page): Promise<void> {
370 await page.evaluate(() => {
371 if ((window as any).__escListenerAdded) return;
372 (window as any).__escListenerAdded = true;
373 (window as any).__stopRecording = false;
374 document.addEventListener('keydown', (e) => {
375 if (e.key === 'Escape') {
376 e.preventDefault();
377 (window as any).__stopRecording = true;
378 }
379 });
380 });
381}
382 
383// Poll for stop signal - handle navigation errors gracefully
384while (!stopped) {
385 try {
386 const shouldStop = await page.evaluate(() => (window as any).__stopRecording === true);
387 if (shouldStop) break;
388 } catch {
389 // Page navigating - continue recording
390 }
391 await new Promise(r => setTimeout(r, 200));
392}
393```
394 
395**Key insight:** `page.evaluate()` throws during navigation. Use try/catch and continue - don't treat errors as stop signals.
396 
397## Window Scaling for Laptops
398 
399Record at full 1080p while showing a smaller window:
400 
401```typescript
402const scale = 0.75; // 75% window size
403const context = await browser.newContext({
404 viewport: { width: 1920 * scale, height: 1080 * scale },
405 deviceScaleFactor: 1 / scale,
406 recordVideo: { dir: './recordings', size: { width: 1920, height: 1080 } },
407});
408```
409 
410## Cookie Banner Dismissal
411 
412Comprehensive selector list for common consent platforms:
413 
414```typescript
415const COOKIE_SELECTORS = [
416 '#onetrust-accept-btn-handler', // OneTrust
417 '#CybotCookiebotDialogBodyButtonAccept', // Cookiebot
418 '.cc-btn.cc-dismiss', // Cookie Consent by Insites
419 '[class*="cookie"] button[class*="accept"]',
420 '[class*="consent"] button[class*="accept"]',
421 'button:has-text("Accept all")',
422 'button:has-text("Accept cookies")',
423 'button:has-text("Got it")',
424];
425 
426async function dismissCookieBanners(page: Page): Promise<void> {
427 await page.waitForTimeout(500);
428 for (const selector of COOKIE_SELECTORS) {
429 try {
430 const btn = page.locator(selector).first();
431 if (await btn.isVisible({ timeout: 100 })) {
432 await btn.click({ timeout: 500 });
433 return;
434 }
435 } catch { /* try next */ }
436 }
437}
438```
439 
440Call after `page.goto()` and on `page.on('load')` for navigation.
441 
442## Important: Injected Elements Appear in Video
443 
444**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).
445 
446## Tips for Good Demo Recordings
447 
4481. **Use slowMo** - 50-100ms makes actions visible
4492. **Add waitForTimeout** - Pause between actions for comprehension
4503. **Wait for animations** - Use `waitForLoadState('networkidle')`
4514. **Match Remotion dimensions** - 1920x1080 at 30fps typical
4525. **Test without recording first** - Debug before final capture
4536. **Clear browser state** - Use fresh context for clean demos
4547. **Dismiss cookie banners** - Use comprehensive selector list above
4558. **Re-inject on navigation** - Cursor/listeners reset on page load
456 
457---
458 
459## Feedback & Contributions
460 
461If this skill is missing information or could be improved:
462 
463- **Missing a pattern?** Describe what you needed
464- **Found an error?** Let me know what's wrong
465- **Want to contribute?** I can help you:
466 1. Update this skill with improvements
467 2. Create a PR to github.com/digitalsamba/claude-code-video-toolkit
468 
469Just say "improve this skill" and I'll guide you through updating `.claude/skills/playwright-recording/SKILL.md`.
470 
View all files on GitHub →

Details

Updated
Sep 21, 2026
License
MIT
Stars on the repo
★ 2,114
Checked
Source
digitalsamba/claude-code-video-toolkit ↗
Creator
digitalsamba · 11 listings
View creator

Similar skills

See all →
SkillMake a polished video of an app or a featureMake a demo video with OpenScreengetopenscreen/openscreen · by getopenscreenMake a polished video of an app or a feature with OpenScreen's MCP tools, from storyboard to recording, editing and export, then either attach it to a pull request or draft a post on X. Use when the user asks for a demo, a demo video, a screen recording, a product video, a launch clip, a video for X/Twitter, LinkedIn or a README, or to show a change in a pull request ("add a video to the PR", "show the feature to reviewers"). Also use to set OpenScreen up for an agent, and to continue an OpenScreen demo in a new session.★ 4.5k GitHub starsSkillVideo and audio processing with FFmpegFFmpeg for Video Productiondigitalsamba/claude-code-video-toolkit · by digitalsambaVideo and audio processing with FFmpeg. Use for format conversion, resizing, compression, audio extraction, and preparing assets for Remotion. Triggers include converting GIF to MP4, resizing video, extracting audio, compressing files, or any media transformation task.★ 2.1k GitHub starsSkillTurn static SCREENSHOTS into a simulated screen recordingfake-screencast — screenshots → simulated screen recordinghassancs91/claude-youtube-editor · by hassancs91Turn static SCREENSHOTS into a simulated screen recording (TSX) — a fake screencast with an animated cursor that eases to targets and clicks, a browser URL bar that updates per page, hard-cut navigations, in-page filter crossfades, smooth scroll, and a ken-burns zoom onto the payoff. Use when a beat needs to show a walkthrough of a web app / dashboard / settings page and you'd rather build it from screenshots than get (or ask for) a real screen recording — "fake a screen recording", "turn these screenshots into a screencast", "simulate clicking through the dashboard", "animate this UI walkthrough", "cursor moving through the app". Built on remotion/src/lib/screencast.tsx. This is a technique WITHIN step 2 (make-tsx): defer timeline/render/bake orchestration to make-tsx and raw crash-free TSX rules to vidtsx-2d-generator. Not for a single static page clone (use WebBrowserFrame directly) and not when the point is genuine proof of real speed/output (use a real recording).★ 330 GitHub starsSkillA Khan Academy style explainer videoKhan explainerhaiderfarooq3/khan-explainer · by haiderfarooq3Use when the user wants a Khan Academy style explainer video, a blackboard or whiteboard lesson, a hand-drawn chalk-talk animation, a short narrated video that teaches a concept ("explain X in a video", "explainer video", "teach X in 30 seconds"), or a tutorial that draws hand-written arrows, circles and captions over product screenshots ("draw over the UI", "annotated walkthrough"), or clips cut from a YouTube video or podcast where the speaker's real audio plays under a blackboard drawn to it ("khanify this video", "clip this with the Khan visual"), especially when it must be free, local, and use no paid APIs or keys. macOS only.★ 113 GitHub starsSkillPick the output format that makes an explanation easiestUnderstanding ladderChangWenC/understanding-ladder · by ChangWenCPick the output format that makes an explanation easiest to understand, following Karpathy's understanding ladder — (1) plain controlled text, (2) diagram, (3) interactive HTML page, (4) 3Blue1Brown-style explainer video. Classify the content (definition/steps, process/causality, parameter→outcome, unfolding derivation), recommend the lowest rung that fully answers, then produce it. Works in English and Chinese. Use when the user says "explain this clearly", "help me understand", "draw a diagram", "make it interactive", "make an explainer video", "what's the best way to explain X", "understanding ladder", "Karpathy ladder", or 「讲清楚」「帮我理解」「画个图」「做个交互页」「做个讲解视频」「用哪种方式讲最好懂」「理解阶梯」; also when explaining a non-trivial concept, mechanism, algorithm, or an AI agent's output.★ 108 GitHub starsSkillAnimate first frames into A-roll talking head clipsAnimate (A-Roll)TheMattBerman/scrollclaw · by TheMattBermanAnimate first frames into A-roll talking head clips. Uses Sora 2 via fal.ai (primary) with Kling 3 auto-fallback when Sora is unavailable or sunset.★ 71 GitHub stars

More from digitalsamba

SkillAI music generation with ACE-Step 1.5ACE-Step 1.5 Music Generationdigitalsamba/claude-code-video-toolkit · by digitalsambaAI music generation with ACE-Step 1.5 — background music, vocal tracks, covers, stem extraction, audio repainting, and continuation for video production. Use when generating music, soundtracks, jingles, or working with audio stems. Triggers include background music, soundtrack, jingle, music generation, stem extraction, cover, style transfer, repaint, continuation, or musical composition tasks.★ 2.1k GitHub starsSkillGenerate AI voiceovers, sound effectsElevenLabs Audio Generationdigitalsamba/claude-code-video-toolkit · by digitalsambaGenerate AI voiceovers, sound effects, and music using ElevenLabs APIs. Use when creating audio content for videos, podcasts, or games. Triggers include generating voiceovers, narration, dialogue, sound effects from descriptions, background music, soundtrack generation, voice cloning, or any audio synthesis task.★ 2.1k GitHub starsSkillVideo and audio processing with FFmpegFFmpeg for Video Productiondigitalsamba/claude-code-video-toolkit · by digitalsambaVideo and audio processing with FFmpeg. Use for format conversion, resizing, compression, audio extraction, and preparing assets for Remotion. Triggers include converting GIF to MP4, resizing video, extracting audio, compressing files, or any media transformation task.★ 2.1k GitHub starsSkillPrompting patterns for Ideogram 4 text-to-imageIdeogram 4 Skilldigitalsamba/claude-code-video-toolkit · by digitalsambaPrompting patterns for Ideogram 4 text-to-image — best-in-class in-image text rendering and exact color/layout control via structured JSON captions. Use when generating images that need legible on-image text (title cards, thumbnails, logos, signage, CTAs), precise brand colors, or controlled spatial layout. Triggers include title slide image, thumbnail with text, on-image text, legible text in image, brand color palette image, bounding-box layout, Ideogram.★ 2.1k GitHub starsSkillAI video generation with LTX-2.3 22BLTX-2.3 Video Generationdigitalsamba/claude-code-video-toolkit · by digitalsambaAI video generation with LTX-2.3 22B — text-to-video, image-to-video clips for video production. Use when generating video clips, animating images, creating b-roll, animated backgrounds, or motion content. Triggers include video generation, animate image, b-roll, motion, video clip, text-to-video, image-to-video.★ 2.1k GitHub starsSkillPython video composition with moviepy 2.xmoviepy for Video Productiondigitalsamba/claude-code-video-toolkit · by digitalsambaPython video composition with moviepy 2.x — overlaying deterministic text on AI-generated video (LTX-2, SadTalker), compositing clips, single-file build.py video projects. Use when adding labels/captions/lower-thirds to LTX-2 or SadTalker outputs, building short ad-style spots in pure Python without Remotion, or doing programmatic video composition. Triggers include text overlay on video, label LTX-2 clip, caption SadTalker output, lower third, build.py video, moviepy, Python video composition, sub-30s ad spot.★ 2.1k GitHub stars