playwright-recording

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

INSTALLATION
npx skills add https://github.com/digitalsamba/claude-code-video-toolkit --skill playwright-recording
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

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

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

// 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 } },

});

Cookie Banner Dismissal

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

  • Use slowMo - 50-100ms makes actions visible
  • Add waitForTimeout - Pause between actions for comprehension
  • Wait for animations - Use waitForLoadState('networkidle')
  • Match Remotion dimensions - 1920x1080 at 30fps typical
  • Test without recording first - Debug before final capture
  • Clear browser state - Use fresh context for clean demos
  • Dismiss cookie banners - Use comprehensive selector list above
  • Re-inject on navigation - Cursor/listeners reset on page load

Feedback &#x26; 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:
  • Update this skill with improvements
  • 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.

BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card