Playwright Video Recording

Record browser interactions as video using Playwright.

Playwright Video Recording — Super Bowl-style launch ad (from the digitalsamba/claude-code-video-toolkit README)

From the digitalsamba/claude-code-video-toolkit README — shows the whole collection, not only this skill. · view on GitHub

How to use it

Claude Code
  1. Run the line below. It pulls the whole folder into ~/.claude/skills/playwright-recording.
  2. Describe your job in plain words. Claude Code follows the skill from there.
Claude Code — installs the whole folder, not just SKILL.md
npx degit digitalsamba/claude-code-video-toolkit/.claude/skills/playwright-recording#main ~/.claude/skills/playwright-recording

For one project only, change the path to .claude/skills/playwright-recording.

Claude (web or desktop app)
  1. On this page open ⋯ → Download .md.
  2. Save it as SKILL.md in a folder, zip the folder, then Customize → Skills → + → Create skill → Upload a skill.
  3. Pick the file and Save. Claude shows the name and description and runs a security scan.
  4. Check the skill is switched on.
  5. Start a new chat and describe your job in plain words. The AI follows the skill from there.
ChatGPT or another app
  1. ChatGPT: make a Project and paste it into Instructions.
  2. Neither? Paste it at the top of a new chat — it works for that chat.
Not working?
  • Check which app you pasted it into — the steps above name the right one.
  • Some skills need the paid tier of Claude or ChatGPT.
Step-by-step guide with screenshots · Ask in the forum

Paste into Claude, ChatGPT or Cursor.

Source of Playwright Video Recording

Show the full text470 lines
namedescription
playwright-recordingRecord 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
# 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 

Discussion

Alternatives

Also in Video productionSee all 320 in Content creator →