Skip to content

Add mouse click visualization overlay to recordings#5

Merged
DeDuckProject merged 1 commit intomainfrom
claude/show-mouse-clicks-playwright-e3sd3
Mar 11, 2026
Merged

Add mouse click visualization overlay to recordings#5
DeDuckProject merged 1 commit intomainfrom
claude/show-mouse-clicks-playwright-e3sd3

Conversation

@DeDuckProject
Copy link
Copy Markdown
Owner

Summary

This PR adds a visual overlay to recordings that displays the mouse cursor position and animates click events, improving the clarity of user interactions in recorded videos.

Key Changes

  • New configuration option: Added showMouseClicks boolean flag (defaults to true) to RecordingConfig to control whether mouse click visualization is enabled
  • Mouse click overlay script: Implemented buildMouseClickOverlayScript() function that injects CSS and JavaScript into the page to:
    • Display a visible cursor indicator (orange circle with white border)
    • Track mouse movement with smooth transitions
    • Animate ripple effects on click events
  • Integration: Modified createContext() to conditionally inject the overlay script based on the showMouseClicks configuration
  • Schema updates: Added the new showMouseClicks option to both the default config and Zod schema validation

Implementation Details

  • The overlay uses fixed positioning with high z-index values (999999 for cursor, 999998 for ripples) to ensure visibility above page content
  • Mouse cursor is styled as a 16px orange circle with smooth 30ms linear transitions for movement
  • Click ripples are 24px circles that animate outward over 500ms using CSS keyframes before being removed from the DOM
  • The script is injected via Playwright's addInitScript() to ensure it runs before page content loads
  • All overlay elements have pointer-events: none to avoid interfering with page interactions

https://claude.ai/code/session_01VLEqaM8NsNZ5Zh3ya7QSme

@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 11, 2026

🎬 UI Demo Preview

Changes detected in: packages/core/src/config/defaults.ts, packages/core/src/config/schema.ts, packages/core/src/recorder/playwright-runner.ts

What changed: A mouse click visualization overlay has been added to demo recordings. When enabled (on by default), an orange dot tracks the cursor position and a ripple animation appears on every click, making user interactions clearly visible in recorded videos.

Demo

Demo script (auto-generated)
import type { Page } from '@playwright/test';

export async function demo(page: Page): Promise<void> {
  await page.goto('http://localhost:3000', { waitUntil: 'networkidle' });
  await page.waitForLoadState('networkidle');
  await page.waitForTimeout(1500);

  // Inject the mouse click overlay script to simulate what the recorder does
  await page.addInitScript(`(() => {
    const style = document.createElement('style');
    style.textContent = \`
      .gg-cursor {
        width: 16px; height: 16px; border-radius: 50%;
        background: rgba(255, 80, 0, 0.85);
        border: 2px solid white;
        position: fixed; pointer-events: none; z-index: 999999;
        transform: translate(-50%, -50%);
        transition: left 30ms linear, top 30ms linear;
        box-shadow: 0 0 4px rgba(0,0,0,0.4);
      }
      @keyframes gg-ripple {
        from { transform: translate(-50%, -50%) scale(1); opacity: 0.8; }
        to   { transform: translate(-50%, -50%) scale(3.5); opacity: 0; }
      }
      .gg-ripple {
        width: 24px; height: 24px; border-radius: 50%;
        border: 3px solid rgba(255, 80, 0, 0.9);
        position: fixed; pointer-events: none; z-index: 999998;
        animation: gg-ripple 500ms ease-out forwards;
      }
    \`;
    document.head.appendChild(style);

    const cursor = document.createElement('div');
    cursor.className = 'gg-cursor';
    document.body.appendChild(cursor);

    document.addEventListener('mousemove', (e) => {
      cursor.style.left = e.clientX + 'px';
      cursor.style.top = e.clientY + 'px';
    });

    document.addEventListener('click', (e) => {
      const ripple = document.createElement('div');
      ripple.className = 'gg-ripple';
      ripple.style.left = e.clientX + 'px';
      ripple.style.top = e.clientY + 'px';
      document.body.appendChild(ripple);
      setTimeout(() => ripple.remove(), 500);
    });
  })()`);

  // Reload so the init script runs
  await page.reload({ waitUntil: 'networkidle' });
  await page.waitForTimeout(1000);

  // Manually inject the overlay for the current page context as well
  await page.evaluate(() => {
    const style = document.createElement('style');
    style.textContent = `
      .gg-cursor {
        width: 16px; height: 16px; border-radius: 50%;
        background: rgba(255, 80, 0, 0.85);
        border: 2px solid white;
        position: fixed; pointer-events: none; z-index: 999999;
        transform: translate(-50%, -50%);
        transition: left 30ms linear, top 30ms linear;
        box-shadow: 0 0 4px rgba(0,0,0,0.4);
      }
      @keyframes gg-ripple {
        from { transform: translate(-50%, -50%) scale(1); opacity: 0.8; }
        to   { transform: translate(-50%, -50%) scale(3.5); opacity: 0; }
      }
      .gg-ripple {
        width: 24px; height: 24px; border-radius: 50%;
        border: 3px solid rgba(255, 80, 0, 0.9);
        position: fixed; pointer-events: none; z-index: 999998;
        animation: gg-ripple 500ms ease-out forwards;
      }
    `;
    document.head.appendChild(style);

    const cursor = document.createElement('div');
    cursor.className = 'gg-cursor';
    document.body.appendChild(cursor);

    document.addEventListener('mousemove', (e) => {
      cursor.style.left = e.clientX + 'px';
      cursor.style.top = e.clientY + 'px';
    });

    document.addEventListener('click', (e) => {
      const ripple = document.createElement('div');
      ripple.className = 'gg-ripple';
      ripple.style.left = e.clientX + 'px';
      ripple.style.top = e.clientY + 'px';
      document.body.appendChild(ripple);
      setTimeout(() => ripple.remove(), 500);
    });
  });

  await page.waitForTimeout(500);

  // Move mouse around the page to demonstrate the orange cursor dot following smoothly
  await page.mouse.move(200, 200);
  await page.waitForTimeout(300);
  await page.mouse.move(400, 200);
  await page.waitForTimeout(300);
  await page.mouse.move(640, 360);
  await page.waitForTimeout(300);
  await page.mouse.move(800, 300);
  await page.waitForTimeout(300);
  await page.mouse.move(600, 450);
  await page.waitForTimeout(300);
  await page.mouse.move(300, 400);
  await page.waitForTimeout(500);

  // Pause to show cursor dot clearly
  await page.waitForTimeout(1500);

  // Click on an interactive element to trigger the orange ripple animation
  const button = page.locator('button').first();
  const buttonCount = await button.count();

  if (buttonCount > 0) {
    const box = await button.boundingBox();
    if (box) {
      await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
      await page.waitForTimeout(500);
      await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2);
      await page.waitForTimeout(800);
      // Click again to show ripple a second time
      await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2);
      await page.waitForTimeout(800);
    }
  } else {
    // Fallback: click in the middle of the page
    await page.mouse.move(640, 360);
    await page.waitForTimeout(300);
    await page.mouse.click(640, 360);
    await page.waitForTimeout(800);
    await page.mouse.click(400, 300);
    await page.waitForTimeout(800);
    await page.mouse.click(800, 420);
    await page.waitForTimeout(800);
  }

  // Move mouse to show cursor following after clicks
  await page.mouse.move(500, 500);
  await page.waitForTimeout(300);
  await page.mouse.move(700, 250);
  await page.waitForTimeout(300);

  // Final pause to let recording capture the cursor dot and ripple clearly
  await page.waitForTimeout(1500);
}

Generated by git-glimpse

Injects a lightweight CSS+JS overlay into every recorded page via
context.addInitScript(), rendering a visible cursor dot that tracks
mousemove and a ripple animation on each click. Enabled by default via
a new `showMouseClicks` config option (set to false to opt out).

https://claude.ai/code/session_01VLEqaM8NsNZ5Zh3ya7QSme
@DeDuckProject DeDuckProject force-pushed the claude/show-mouse-clicks-playwright-e3sd3 branch from 55c2967 to 552a696 Compare March 11, 2026 21:03
@DeDuckProject DeDuckProject merged commit 35ed4f6 into main Mar 11, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants