Animations via JavaScript

This section documents modern ECMAScript and core browser JavaScript APIs — it is not tied to any specific framework or library (React, Vue, Angular, etc.). This content was generated with the assistance of AI and should be verified against the current ECMAScript specification and MDN documentation before relying on it in production, since JavaScript language features and browser API support continue to evolve.

This section’s bibliography lists the reference material consulted while preparing these pages.

The reference book has only light coverage of this topic — a single subsection (§15.4.5, on reacting to CSS transition/animation events from JavaScript) — so most of this page draws on general/MDN knowledge instead. That narrow book subsection is already covered in full on Accessing CSS from JavaScript (see "Reacting to Animation and Transition Events" there); this page instead covers the broader landscape of driving animation from JavaScript: toggling CSS state, the requestAnimationFrame loop, the Web Animations API, and animating SVG.

Driving CSS Animations and Transitions from JavaScript

The simplest way for JavaScript to animate something is to not animate it directly at all — instead, change a small piece of state and let a CSS transition or @keyframes animation (see Transitions and Animations & Keyframes) interpolate the visual change. The browser’s compositor drives the frames, which keeps the animation smooth even if the main JavaScript thread is briefly busy.

Toggling classes

Adding or removing a class is the most common trigger, using the classList API covered in Accessing CSS from JavaScript:

// .card { transition: transform .2s ease-out; }
// .card.raised { transform: translateY(-4px) scale(1.02); }
card.classList.add("raised");      // starts the transition
card.classList.remove("raised");   // reverses it

This keeps the animation’s actual definition (durations, easing, keyframes) in the stylesheet, and the JavaScript declarative — it says what state the element is in, not how to get there frame by frame. Listen for transitionend/animationend (also covered on that page) to know when the animation has actually finished, e.g. before removing the element from the DOM or re-enabling a button.

Toggling custom properties

A CSS custom property (variable) can parameterize an animation or transition without needing a different class for every possible value. This is especially useful when the target value is computed at runtime rather than being one of a small fixed set:

.progress-bar {
  --progress: 0;
  width: calc(var(--progress) * 1%);
  transition: width 0.3s ease-out;
}
function setProgress(bar, percent) {
  bar.style.setProperty("--progress", percent); // triggers the CSS transition on `width`
}
setProgress(progressBar, 75);

A custom property is not itself animatable by default — only the concrete property that consumes it (width above) actually transitions, unless --progress is registered with @property (declaring a syntax and initial-value), in which case the custom property itself can be transitioned/animated directly.

The requestAnimationFrame Loop

Class and custom-property toggling only goes so far: it can’t express an animation whose path depends on continuous input (dragging, physics, a value that changes every frame based on other state). For that, JavaScript drives the animation itself, one frame at a time, using window.requestAnimationFrame().

requestAnimationFrame(callback) asks the browser to call callback right before its next repaint — typically 60 times per second, matching the display’s refresh rate, and paused automatically while the tab is in a background/hidden state. callback receives a single argument: a high-resolution timestamp (milliseconds, same clock as performance.now()) representing the moment the frame started. Each call schedules only one future frame, so a continuous animation must call requestAnimationFrame again from inside the callback itself.

Worked example: animating a value over time

The key to smooth, frame-rate-independent motion is using the timestamp argument to compute elapsed time, rather than assuming a fixed amount of progress happens on every call — a monitor running at 144Hz calls the callback far more often than one running at 60Hz, and a slow frame (a GC pause, a busy tab) must not cause the animation to visibly jump:

function animateTo(element, toX, duration = 400) {
  const fromX = parseFloat(getComputedStyle(element).translate) || 0;
  const delta = toX - fromX;
  let startTime = null;
  let frameId;

  function step(timestamp) {
    if (startTime === null) startTime = timestamp;
    const elapsed = timestamp - startTime;
    const progress = Math.min(elapsed / duration, 1);   // clamp to [0, 1]
    const eased = 1 - Math.pow(1 - progress, 3);          // ease-out cubic

    element.style.translate = `${fromX + delta * eased}px`;

    if (progress < 1) {
      frameId = requestAnimationFrame(step);
    }
  }

  frameId = requestAnimationFrame(step);
  return () => cancelAnimationFrame(frameId); // caller can cancel mid-flight
}

const cancel = animateTo(document.querySelector("#puck"), 300);
// cancel();   // stop the animation before it completes, e.g. on a new user interaction

A few points worth calling out:

  • Never accumulate a fixed step per frame (x += 2 inside the callback) — that ties speed to the display’s refresh rate. Always derive progress from the elapsed time, as above, so the animation takes the same duration on a 60Hz and a 144Hz display alike.

  • cancelAnimationFrame(id), given the ID returned by the matching requestAnimationFrame call, cancels a frame that hasn’t fired yet — essential when the same element might be re-animated before a previous animation finishes, or when the element is removed from the DOM mid-animation.

  • A requestAnimationFrame loop is also the standard way to drive per-frame drawing on <canvas> — see Canvas, WebGL & Three.js for that render-loop pattern.

  • Respect the user’s prefers-reduced-motion setting (matchMedia("(prefers-reduced-motion: reduce)").matches) by skipping or shortening non-essential JavaScript-driven animation, the same way a CSS animation should.

The Web Animations API

The Web Animations API is a native, imperative alternative to both CSS-class toggling and hand-rolled requestAnimationFrame loops: it lets JavaScript describe a keyframe animation directly, while still letting the browser’s own compositor run and interpolate it (no per-frame JavaScript callback required).

element.animate()

Every Element exposes animate(keyframes, options), which starts the animation immediately and returns an Animation object representing it:

const banner = document.querySelector("#banner");

const animation = banner.animate(
  [
    { transform: "translateY(-100%)", opacity: 0 },   // implicit offset 0
    { transform: "translateY(0)", opacity: 1 },        // implicit offset 1
  ],
  {
    duration: 400,
    easing: "ease-out",
    fill: "forwards",   // keep the end-state styling applied once the animation finishes
  },
);

Keyframes can be given as an array of state objects (as above, evenly spaced by default, or explicitly positioned with an offset field between 0 and 1 on each), or equivalently as a single object whose properties are each an array of values:

banner.animate(
  { transform: ["translateY(-100%)", "translateY(0)"], opacity: [0, 1] },
  { duration: 400, easing: "ease-out", fill: "forwards" },
);

Common options fields:

Option Meaning

duration

Length of one iteration, in milliseconds.

easing

A CSS easing function ("ease-out", "linear", "cubic-bezier(…​)", "steps(4)", …​).

iterations

Number of times to repeat; Infinity for a looping animation.

direction

"normal", "reverse", "alternate", or "alternate-reverse" (for repeated iterations).

delay / endDelay

Milliseconds to wait before starting / after finishing before considering the animation done.

fill

Whether the first/last keyframe’s styling applies outside the animation’s active interval: "none" (default), "forwards", "backwards", or "both".

Controlling the returned Animation object

Unlike a CSS class toggle, the object returned by animate() can be paused, resumed, reversed, or inspected at any point — there is no need to separately track "is this element mid-animation":

const spin = logo.animate(
  [{ transform: "rotate(0deg)" }, { transform: "rotate(360deg)" }],
  { duration: 1200, iterations: Infinity },
);

spin.pause();               // freeze on the current frame
spin.play();                // resume from where it was paused
spin.playbackRate = 2;      // play at double speed (negative values play backwards)
spin.reverse();              // swap direction, continuing from the current position
spin.cancel();               // stop and remove all animation effects, reverting to pre-animation styling
// spin.finish();             // jump immediately to the end -- throws `InvalidStateError` here, since
                               // `finish()` requires a finite animation and `spin` uses `iterations: Infinity`;
                               // use `cancel()` above to stop an infinite animation instead

// `finished` is a Promise that resolves once the animation completes (or rejects if cancelled)
spin.finished.then(() => console.log("spin finished"));
spin.onfinish = () => console.log("spin finished"); // equivalent event-based alternative

currentTime (milliseconds into the animation) and playState ("idle", "running", "paused", or "finished") can both be read and, for currentTime, written to scrub the animation directly — handy for tying an animation’s progress to something else, such as a scroll position or a drag gesture.

Choosing between the three approaches

Approach Best for Drawback

CSS class/custom-property toggle

Fixed, stylesheet-defined states and transitions; the common case

Only expresses states known ahead of time; JS can’t query fine-grained progress

requestAnimationFrame loop

Continuous, input-driven, or physically-simulated motion; canvas drawing

Runs on the main thread every frame; more code to get right (easing, cancellation)

Web Animations API (element.animate())

Keyframe animations that need imperative control (pause/reverse/scrub) without a per-frame callback

Keyframes are still declared in JS, not the stylesheet, so designers can’t tweak them independently

The three can also be mixed — e.g. element.animate() for a visual effect while a requestAnimationFrame loop separately drives an unrelated physics calculation.

Animating SVG from JavaScript

Inline SVG shapes (see Styling & Animating SVGs for the CSS-only approach, including the stroke-dashoffset line-draw technique and the legacy SMIL syntax) can be animated with either of the JavaScript techniques above, since an inlined SVG element is a normal part of the page’s DOM:

const circle = document.querySelector(".icon-check__circle");
const length = circle.getTotalLength();   // exact path length, no manual measuring needed

circle.style.strokeDasharray = `${length}`;
circle.style.strokeDashoffset = `${length}`;

circle.animate(
  [{ strokeDashoffset: length }, { strokeDashoffset: 0 }],
  { duration: 600, easing: "ease-out", fill: "forwards" },
);

A few SVG-specific notes when animating this way:

  • getTotalLength() (available on <path>, <circle>, <rect>, and other shape elements) removes the need to measure or guess a path’s length up front, which the pure-CSS technique on Styling & Animating SVGs has to do manually.

  • CSS-animatable SVG properties (stroke-dashoffset, fill, opacity, and geometry properties like cx/cy/r in current evergreen browsers) work with element.animate() exactly as they do with classList toggling or a CSS @keyframes rule — the Web Animations API simply triggers the same underlying animation machinery.

  • Prefer setting SVG-specific values through the CSS property rather than the matching XML attribute when animating from JavaScript, for the cascade/specificity reasons discussed on that page.

  • For animating an SVG’s transform, remember transform-box: fill-box may be needed so transform-origin centers on the shape itself rather than the SVG viewport’s origin — also covered on that page.

For anything beyond simple shape/property animation — large numbers of independently moving elements, particle effects, or pixel-level manipulation — rendering to <canvas> instead of the DOM is usually a better fit; see Canvas, WebGL & Three.js.