A timer does not know when the browser is about to paint. requestAnimationFrame schedules a callback before a future frame, making it a better fit for visual updates such as moving an element or drawing on a canvas.

let frameId;
let start;

function animate(now) {
  start ??= now;
  const progress = Math.min((now - start) / 400, 1);
  element.style.transform = `translateX(${progress * 120}px)`;
  if (progress < 1) frameId = requestAnimationFrame(animate);
}

frameId = requestAnimationFrame(animate);

Use the timestamp, not a fixed step

  • Base progress on the callback timestamp so motion is not tied to a particular refresh rate.
  • Keep layout reads and writes organized to avoid repeated forced layout.
  • Cancel a scheduled frame with cancelAnimationFrame when the animation should stop.

Respect the user's motion preference

Offer a reduced-motion path for non-essential movement and avoid animating content that can distract or make the page uncomfortable.

Test at different refresh rates and when a tab is in the background. Browsers may pause or throttle animation callbacks when a page is not visible.