Cursor Overlay
Add an animated cursor that moves to click positions in the video.
The .cursorOverlay() stage renders an animated cursor that travels from one click position to the next and reaches each target at its trace timestamp. You can control the travel duration, easing, and how long the cursor remains visible after arrival.
Basic usage
await Recast
.from('./traces')
.parse()
.cursorOverlay()
.render({ format: 'mp4' })
.toFile('demo.mp4')Configuration options
| Option | Type | Default | Description |
|---|---|---|---|
image | string | bundled arrow cursor | Path to a custom cursor image (PNG) |
size | number | 24 | Cursor width in pixels (relative to 1080p) |
color | string | '#FFFFFF' | Cursor color |
opacity | number | 0.9 | Cursor opacity from 0 to 1 |
easing | 'linear' | 'ease-in-out' | 'ease-out' | 'ease-in-out' | Easing used while travelling between positions |
moveDurationMs | number | 250 | Maximum time spent travelling to the next position |
hideAfterMs | number | 500 | Time the cursor remains visible after reaching a position |
shadow | boolean | true | Show a drop shadow on the default cursor |
approachMs | number | 500 | Hold (freeze) duration for marker-driven clicks — see Held cursor approach |
filter | (action) => boolean | — | Select which actions generate cursor positions |
.cursorOverlay({
easing: 'ease-out',
moveDurationMs: 300,
hideAfterMs: 600,
})moveDurationMs is a maximum: closely spaced positions use the shorter interval between their timestamps. Values below 50 ms use a 50 ms safety floor for ffmpeg interpolation. Negative hideAfterMs values behave as 0.
Custom cursor image
.cursorOverlay({
image: './assets/custom-cursor.png',
size: 40,
})The bundled default is a 30x44 arrow cursor PNG. Provide your own image for branded or stylized cursors.
approachMs — held cursor approach
By default the cursor's approach is timed off each click action detected in the trace. For clicks that wait on a page load, that approach can glide over a still-loading screen before the target paints — "the mouse moves before there's anything to click on."
The click() / markClick() test helpers fix this by writing an explicit click marker. For each marker, the renderer holds (freezes) the painted frame at the click for approachMs and lets the cursor play its full glide over it, then resumes into the click's result. approachMs defaults to 500 ms; keep it at least as large as moveDurationMs so the full movement fits inside the hold.
.cursorOverlay({ approachMs: 600 })approachMs only affects marker-driven clicks; plain auto-detected clicks are unaffected. With a .voiceover() stage, the hold also extends the audio and subtitles in lockstep so narration stays in sync.
How it works
For each click action detected in the trace:
- The first cursor position arrives from a small upper-left offset
- Later positions travel from the preceding pointer position with the configured easing
- The cursor reaches the target at the click timestamp and hides after
hideAfterMs
The animation uses ffmpeg movie + overlay filters with per-position enable expressions and easing calculated via st()/ld() temp variables. If Playwright spent a long time auto-waiting for a target to appear, the cursor appears directly at that target instead of gliding over the loading screen.
CLI equivalent
# Enable with defaults
npx playwright-recast -i ./traces --cursor-overlay
# With custom config
npx playwright-recast -i ./traces --cursor-overlay-config cursor.jsonTips
- Cursor overlay works well combined with Click Effect — the cursor moves into position, then the ripple appears on click.
- Use the
click()helper in your test for the clicks you want to emphasise — the renderer gives those a held, deliberate approach (approachMs) over the painted target. - Cursor positions are remapped through speed processing, so the animation timing stays accurate after speed changes.
- Only actions from the recording context are processed — setup/background actions are filtered out automatically.
- The overlay is applied before zoom cropping, so cursor positions stay accurate during zoomed segments.