← Blog

Animated cursor and click effects in Remotion

Animate a mouse cursor that moves between waypoints and clicks in Remotion — interpolating x and y from the frame, an eased path, a click ring — then a drop-in cursor that walks over any UI or screen recording.

Short answer: a cursor animation in Remotion is an SVG pointer positioned by interpolate() over a list of waypoints — frame, x, y — with an ease and clamped ends. A click is a ring that grows and fades from the frame the pointer arrives. Put the cursor on the top layer, over the UI or screen recording. Or install Cursor Track, which wraps any children and takes waypoints with click: true.

A cursor from scratch

Positions as fractions of the frame, so the path survives a change of resolution:

import { AbsoluteFill, Easing, interpolate, useCurrentFrame, useVideoConfig } from "remotion";

// Frame the cursor arrives, and where. Fractions of the frame.
const PATH = [
  { at: 0, x: 0.2, y: 0.8 },
  { at: 30, x: 0.45, y: 0.4 },
  { at: 70, x: 0.7, y: 0.6 },
];
const CLICKS = [30, 70]; // click on arrival

export const Cursor = () => {
  const frame = useCurrentFrame();
  const { width, height } = useVideoConfig();

  const frames = PATH.map((p) => p.at);
  const ease = { easing: Easing.inOut(Easing.cubic), extrapolateLeft: "clamp", extrapolateRight: "clamp" } as const;
  const x = interpolate(frame, frames, PATH.map((p) => p.x), ease) * width;
  const y = interpolate(frame, frames, PATH.map((p) => p.y), ease) * height;

  // The most recent click, if it is still animating.
  const last = CLICKS.filter((c) => c <= frame).at(-1);
  const t = last === undefined ? 1 : interpolate(frame - last, [0, 14], [0, 1], { extrapolateRight: "clamp" });

  return (
    <AbsoluteFill>
      <div
        style={{
          position: "absolute",
          left: x,
          top: y,
          width: 48,
          height: 48,
          marginLeft: -24,
          marginTop: -24,
          borderRadius: "50%",
          border: "3px solid #3b82f6",
          transform: `scale(${0.4 + t * 1.2})`,
          opacity: 1 - t,
        }}
      />
      <svg
        width={28}
        height={28}
        viewBox="0 0 24 24"
        style={{ position: "absolute", left: x, top: y }}
      >
        <path
          d="M3 2 L3 19 L8 14.5 L11.5 22 L14.5 20.7 L11 13.3 L17.5 13.3 Z"
          fill="#09090b"
          stroke="#ffffff"
          strokeWidth={1.5}
          strokeLinejoin="round"
        />
      </svg>
    </AbsoluteFill>
  );
};

The details that make it read as a real pointer:

  • The hotspot is the tip. The SVG's top-left is the point of the arrow, so left: x, top: y puts the tip exactly on the target, and the ring is centred there too.
  • Ease in and out. A linear cursor looks robotic; a real hand accelerates and settles. Easing.inOut(Easing.cubic) is enough.
  • An outline, not a shadow. A white stroke around a dark arrow stays readable over any footage.
  • Clamp both ends. Before the first waypoint and after the last, the cursor should rest, not fly off.

With several waypoints, the easing applies to each segment, so the cursor slows into every point and sets off again straight away. To make it hold, add a second entry at the same position with a later frame.

The drop-in cursor

npx shadcn@latest add @snapcn/cursor-track
import { CursorTrack } from "@/components/snap-cn/cursor-track";
import { ScreenRecording } from "@/components/snap-cn/screen-recording";

export const CheckoutDemo = () => (
  <CursorTrack
    path={[
      { at: 0, x: -0.06, y: 0.86, duration: 6 },
      { at: 8, x: 0.34, y: 0.44, duration: 22, click: true },
      { at: 52, x: 0.68, y: 0.62, duration: 20, click: true },
    ]}
  >
    <ScreenRecording src="/recordings/checkout.mp4" camera={[]} />
  </CursorTrack>
);

The waypoint model is slightly different from the one above, and easier to time: at is the frame the cursor starts travelling to the point, duration is how long the move takes, and the cursor rests there until the next at — the hold is the gap. click: true pulses the ring on arrival. An x outside 0–1 on the first waypoint walks the cursor in from off-frame.

PropWhat it does
variantarrow for a desktop pointer, dot for a touch puck on mobile sims
sizePointer size; defaults to 3.9% of the frame height, the size of a real one
ringColorThe click ring; defaults to the theme's primary
clickFramesHow long the ring lasts (14 frames by default)

Put the recording inside the cursor, not the other way round — the pointer has to be the top layer. Waypoints are frame coordinates, so if the recording has its own camera push-in, keyframe the cursor to match or click on a shot that is holding still.

Roster Grant is a finished scene with the click already choreographed in. More in the cursor animations collection.

Next: terminal and code-typing animation in Remotion.

FAQ

How do you animate a mouse cursor in Remotion?

Define waypoints with a frame, an x and a y, and use interpolate() on the current frame over the waypoint frames to get the cursor position, with an easing curve and clamped extrapolation. Render an SVG pointer at that position on top of the scene.

How do you show a click in a Remotion video?

On the frame the cursor arrives, start a ring at the cursor's tip that grows and fades over about half a second, both computed from the number of frames since the click. A small scale dip on the pointer itself helps too.

Why is the cursor missing from my screen recording?

Many capture tools leave the pointer out of the file. Add a synthetic cursor on top of the recording in Remotion instead, which also lets you choose exactly where it goes and when it clicks.

Should the cursor be an emoji or a font glyph?

No. A glyph renders in whatever font the renderer resolves, so it changes between machines. Draw the pointer as an SVG with its hotspot at the tip.