Pipeline Stages

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

OptionTypeDefaultDescription
imagestringbundled arrow cursorPath to a custom cursor image (PNG)
sizenumber24Cursor width in pixels (relative to 1080p)
colorstring'#FFFFFF'Cursor color
opacitynumber0.9Cursor opacity from 0 to 1
easing'linear' | 'ease-in-out' | 'ease-out''ease-in-out'Easing used while travelling between positions
moveDurationMsnumber250Maximum time spent travelling to the next position
hideAfterMsnumber500Time the cursor remains visible after reaching a position
shadowbooleantrueShow a drop shadow on the default cursor
approachMsnumber500Hold (freeze) duration for marker-driven clicks — see Held cursor approach
filter(action) => booleanSelect 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:

  1. The first cursor position arrives from a small upper-left offset
  2. Later positions travel from the preceding pointer position with the configured easing
  3. 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.json

Tips

  • 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.

On this page