🎯

playwright-recording

🎯Skill

from digitalsamba/claude-code-video-toolkit

VibeIndex|
What it does

playwright-recording skill from digitalsamba/claude-code-video-toolkit

πŸ“¦

Part of

digitalsamba/claude-code-video-toolkit(6 items)

playwright-recording

Installation

npxRun with npx
npx playwright install chromium
npxRun with npx
npx ts-node scripts/record-demo.ts
πŸ“– Extracted from docs: digitalsamba/claude-code-video-toolkit
25Installs
31
-
Last UpdatedJan 25, 2026

Skill Details

SKILL.md

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.

Overview

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