Add mouse click visualization overlay to recordings#5
Merged
DeDuckProject merged 1 commit intomainfrom Mar 11, 2026
Merged
Conversation
🎬 UI Demo PreviewChanges detected in: 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 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
55c2967 to
552a696
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

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
showMouseClicksboolean flag (defaults totrue) toRecordingConfigto control whether mouse click visualization is enabledbuildMouseClickOverlayScript()function that injects CSS and JavaScript into the page to:createContext()to conditionally inject the overlay script based on theshowMouseClicksconfigurationshowMouseClicksoption to both the default config and Zod schema validationImplementation Details
addInitScript()to ensure it runs before page content loadspointer-events: noneto avoid interfering with page interactionshttps://claude.ai/code/session_01VLEqaM8NsNZ5Zh3ya7QSme