Click Effects and Cursor
Add visual click ripples, click sounds, and animated cursor overlays to your demo videos
This guide shows how to highlight user interactions with animated click ripples, click sounds, and a visible cursor that moves between action positions.
Click effects
The .clickEffect() stage detects click and selectOption actions from the Playwright trace and renders an animated ripple at each click position.
Basic usage
await Recast
.from('./traces')
.parse()
.clickEffect()
.render({ format: 'mp4' })
.toFile('demo.mp4')With defaults, this adds a blue ripple that expands to 30px radius over 400ms at each click position.
Customizing the ripple
.clickEffect({
color: '#3B82F6', // Ripple color (hex). Default: '#3B82F6' (blue)
opacity: 0.5, // Ripple opacity 0.0-1.0. Default: 0.5
radius: 30, // Max radius in px (relative to 1080p). Default: 30
duration: 400, // Animation duration in ms. Default: 400
})The ripple appears as an expanding circle that fades out over the animation duration. The radius is relative to 1080p -- it scales proportionally at other resolutions.
Adding click sounds
Enable the bundled click sound or provide your own:
// Bundled default click sound
.clickEffect({ sound: true })
// Custom click sound file
.clickEffect({ sound: './assets/click.mp3', soundVolume: 0.8 })The soundVolume option controls the click sound level from 0.0 (silent) to 1.0 (full volume). Default is 0.8.
Click sound timing is automatically remapped through speed processing, so sounds play at the correct video time even when idle periods are compressed.
Filtering clicks
By default, all click and selectOption actions with cursor coordinates are highlighted. To filter:
// Only highlight click actions, not selectOption
.clickEffect({
filter: (action) => action.method === 'click',
})
// Skip clicks on the navigation bar
.clickEffect({
filter: (action) => !action.title.includes('navigation'),
})Cursor overlay
The .cursorOverlay() stage renders an animated cursor that appears before each action, moves to the action position with easing, then disappears after a timeout.
Basic usage
await Recast
.from('./traces')
.parse()
.cursorOverlay()
.render({ format: 'mp4' })
.toFile('demo.mp4')Customizing the cursor
.cursorOverlay({
size: 24, // Cursor size in px (relative to 1080p). Default: 24
color: '#FFFFFF', // Dot color (hex). Default: '#FFFFFF'
opacity: 0.9, // Opacity 0.0-1.0. Default: 0.9
shadow: true, // Drop shadow. Default: true
easing: 'ease-out', // Movement easing. Default: 'ease-in-out'
hideAfterMs: 500, // Fade out delay after last action. Default: 500
})Custom cursor image
Replace the default dot with a custom cursor image:
.cursorOverlay({
image: './assets/cursor-arrow.png', // PNG with transparency
})The image should be a PNG with an alpha channel. It will be rendered at the size dimension.
Filtering actions
Control which trace actions generate cursor positions:
.cursorOverlay({
filter: (action) => action.method === 'click' || action.method === 'fill',
})Combining click effects and cursor
Both stages work together. Add the cursor for movement visualization and click effects for interaction highlighting:
await Recast
.from('./traces')
.parse()
.cursorOverlay({
color: '#FFFFFF',
opacity: 0.9,
easing: 'ease-out',
})
.clickEffect({
color: '#3B82F6',
opacity: 0.5,
sound: true,
})
.render({ format: 'mp4' })
.toFile('demo.mp4')The cursor animates to the click position, then the ripple effect plays at that position. This creates a natural "point and click" visual pattern.
Polished click approach with the click() helper
Auto-detected clicks time the cursor approach off the trace. For clicks that wait on a page load, the cursor can start gliding before the target paints — it appears to move over a still-loading screen. The click() / markClick() test helpers fix this by marking the click explicitly.
In your test, swap locator.click() for the helper:
import { click } from 'playwright-recast'
// Settle → mark → real click. Click options pass through to Playwright.
await click(page.getByRole('button', { name: 'Get started' }))For each marked click, the renderer holds the painted frame at the click and lets the cursor play its full approach over it, then fires the ripple — a deliberate "point and click" beat instead of a glide over a loading screen. The hold duration is approachMs on cursorOverlay:
.cursorOverlay({ approachMs: 600 }) // default 500ms
.clickEffect({ sound: true })A marker that lands near an auto-detected click suppresses it, so you never get a duplicate ripple; if several markers compete for the same auto-detected click, the nearest eligible marker wins. With a voiceover() stage, the hold also extends the audio and subtitles so narration stays in sync across held clicks. See the click() helper reference for details.
With speed processing
Click effects and cursor overlays automatically remap their timestamps through speed processing. Add .speedUp() before them and timing stays correct:
await Recast
.from('./traces')
.parse()
.speedUp({ duringIdle: 4.0, duringUserAction: 1.0 })
.cursorOverlay()
.clickEffect({ sound: true })
.render({ format: 'mp4' })
.toFile('demo.mp4')CLI usage
# Click effects with defaults
npx playwright-recast -i ./traces --click-effect
# Click effects with custom sound
npx playwright-recast -i ./traces --click-effect --click-sound click.mp3
# Click effects from JSON config
npx playwright-recast -i ./traces --click-effect-config config.json
# Cursor overlay
npx playwright-recast -i ./traces --cursor-overlay
# Cursor overlay from JSON config
npx playwright-recast -i ./traces --cursor-overlay-config config.jsonNext steps
- Zoom and Focus -- zoom into action targets for emphasis
- Speed Control -- compress idle periods between interactions
- Click Effect reference -- full pipeline stage documentation
- Cursor Overlay reference -- full pipeline stage documentation
click()/markClick()helpers -- mark clicks for a polished, held cursor approach