cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

Animation with JavaScript

Make browser motion feel smooth with requestAnimationFrame, elapsed-time loops, cubic-bezier easing, CSS animation choices, and the Web Animations API.

By the end you can
  • 01
    Drive a frame loopUse requestAnimationFrame, its timestamp, and cancelAnimationFrame without assuming a fixed refresh rate.
  • 02
    Shape motionExplain easing, cubic Bézier control points, overshoot, and how timing functions change perceived speed.
  • 03
    Pick the right toolChoose CSS transitions, CSS keyframes, element.animate(), or a JavaScript frame loop for real interface tasks.

Smooth motion is scheduled, measured, and kind

Animation is not “change the number really fast.” Good browser animation cooperates with the browser’s paint cycle, measures real elapsed time, and respects people who ask for less motion. That is why this lesson starts with requestAnimationFrame, then adds timing functions, tool choice, and element.animate().

You already know from Timers that setInterval runs on a timer, not on the browser’s projector. For a moving interface, the better question is: “When is the browser ready to draw the next frame, and how much time passed since the last one?”

Real-life analogyA flipbook follows the screen

A flipbook shows one page at a time. requestAnimationFrame lets the browser ask for the next page when it is ready to draw. A timer can call you before that moment.

In real life: One new page is ready to draw
In JavaScript: requestAnimationFrame(frame) runs before repaint
In real life: You check the time since the last page
In JavaScript: The callback timestamp measures elapsed time
In real life: A timer rings at its own pace
In JavaScript: setInterval can drift, bunch up, or waste frames

Where the analogy stops: A flipbook has pages you turn yourself. The browser chooses when it is ready to draw the next frame.

One-line definition

JavaScript animation is code that changes visual state over time, ideally once per browser repaint, using measured time instead of assuming every frame takes the same duration.

requestAnimationFrame

INTERACTIVE

requestAnimationFrame(callback) asks the browser to call callback before the next repaint. The callback receives one DOMHighResTimeStamp. Every rAF callback that runs in the same frame receives the same timestamp, which keeps separate pieces of an animation in sync. The frame rate follows the display: 60 Hz, 120 Hz, variable refresh, or a throttled background tab are all possible.

The shape of a frame loopPop out in the code editor (opens in a new tab)JavaScript
let rafId;let previous;let position = 0;const speed = 0.12; // pixels per millisecond function frame(timestamp) {  const elapsed = previous === undefined ? 0 : timestamp - previous;  previous = timestamp;  position += speed * elapsed;  dot.style.transform = `translateX(${position}px)`;  rafId = requestAnimationFrame(frame);} rafId = requestAnimationFrame(frame);// Later: cancelAnimationFrame(rafId);

The key line is not the transform. It is the elapsed-time calculation. A fixed amount per frame only looks correct on the refresh rate you guessed. Elapsed time says “move at this many pixels per millisecond,” so a slow frame moves farther and keeps the object on schedule.

Frame loop lab: projector-synced motion
A requestAnimationFrame loopPop out in the code editor (opens in a new tab)JavaScript
let rafId;let previous;let position = 0;const speed = 0.12; // pixels per millisecond function frame(timestamp) {  const elapsed = previous === undefined ? 0 : timestamp - previous;  previous = timestamp;  position += speed * elapsed;  dot.style.transform = `translateX(${position}px)`;  rafId = requestAnimationFrame(frame);} rafId = requestAnimationFrame(frame);// Later: cancelAnimationFrame(rafId);
Tracknot measured
Mode
elapsed time
Position
0.0 px
Slow frame
not yet
Try it yourself

Press Start when you are ready. Nothing moves on load, and Reset cancels the stored request id with cancelAnimationFrame.

requestAnimationFrame follows the display, so the measured rate may be 60, 90, 120, or another value. In most browsers it pauses or slows in background tabs.

The lab never starts motion on load. A production page should make the same choice unless the motion is essential and safe. Let the reader press Play, and always keep the request id so you can call cancelAnimationFrame(id) on pause, reset, or cleanup.

Replay: the same timestamps, two movement formulas
Step 0 of 9Ready
Your turn: follow the blue line

Switch the setting and replay. The same timestamps show why elapsed-time movement survives dropped frames better than fixed-per-frame movement.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
let position = 0;let previous = timestamps[0];for (const timestamp of timestamps.slice(1)) {  const elapsed = timestamp - previous;  position += 0.12 * elapsed;  console.log(position.toFixed(2));  previous = timestamp;}
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
Movement formula
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

Timing functions & Bézier curves

PLAYGROUND

Constant speed is rarely what people expect. A drawer feels heavier if it starts gently and stops gently. A warning shake feels sharp because it accelerates quickly. Timing functions map linear time, from 0 to 1, to visual progress, also usually from 0 to 1.

Real-life analogyA drawer slows near the end

A drawer can start gently, slow near the end, or move evenly. ease-in, ease-out, and linear name those choices. A cubic-bezier() curve lets you choose a custom feel.

In real life: A drawer starts slowly
In JavaScript: ease-in begins slowly
In real life: A drawer slows near the end
In JavaScript: ease-out ends slowly
In real life: A drawer moves evenly
In JavaScript: linear changes at a constant rate
In real life: A chosen sliding feel
In JavaScript: Cubic Bézier points shape the curve

Where the analogy stops: A real drawer has weight and friction. An easing function is a math curve, not a physics simulation.

Bézier lab: bend the speed curve
Pure cubic-bezier helperPop out in the code editor (opens in a new tab)JavaScript
const easing = cubicBezier(0.25, 0.1, 0.25, 1);console.log(easing(0));console.log(easing(0.5).toFixed(3));console.log(easing(1));
cubic-bezier(0.25, 0.1, 0.25, 1)copyable easing
Try it yourself

The x control points are clamped to 0..1 because time must move forward. The y points can overshoot below 0 or above 1, which is how bounce-like easing happens.

Move the sliders, copy the cubic-bezier(...) string, then use it in CSS transitions, CSS keyframes, or element.animate() timing.
Feature note: linear() easing

Modern browsers also have a CSS linear() easing function for piecewise linear curves. If you show it in production UI, feature-detect with CSS.supports() after mount because older browsers may not understand it.

CSS vs JavaScript animation

SORT

CSS animation is often the smoothest and simplest tool when the path is known. A transition or @keyframes animation can let the browser do the interpolation; transforms and opacity can often run on the compositor. JavaScript is for decisions: live measurements, physics, canvas drawing, dynamic keyframes, and playback controls.

Real-life analogyA cartoon plays itself or is drawn live

CSS is like a cartoon that plays planned scenes by itself. JavaScript is like drawing each picture as you go when the next picture depends on live information.

In real life: A cartoon plays the planned scenes
In JavaScript: CSS keyframes let the browser animate
In real life: You draw the next picture live
In JavaScript: JavaScript can decide each frame
In real life: The scenes are already known
In JavaScript: CSS suits known motion
In real life: Each picture needs a new decision
In JavaScript: JS gives control but uses main-thread time

Where the analogy stops: Both choices still use the same browser. JavaScript can also ask the browser to manage an animation with the Web Animations API.

Choosing an animation tool
ToolUse it forStrengthWatch out
CSS transitionState changes such as hover, open/closed panels, color or transform changesBrowser can optimize many transform and opacity animationsHard to make frame-by-frame decisions
CSS @keyframesReusable decorative or loading animations with known keyframesDeclare the song once; the browser plays itLogic and live physics do not belong here
element.animate()Dynamic keyframes with browser-managed playback controlsReturns an Animation remote: play, pause, reverse, finishStill think about cleanup and fill
requestAnimationFrameCanvas loops, games, scroll-linked math, physics, measuring every frameCallback gets a frame timestamp synced to the displayYou own the loop and main-thread cost
Which animation tool fits?
  • Button grows slightly when hovered
  • Open and close a drawer after a class changes
  • Reusable loading spinner
  • Attention pulse that repeats three times
  • Toast slides in with a duration chosen from settings
  • Pause, reverse, and finish a tutorial card animation
  • Game sprite moves from velocity and collisions
  • Physics model follows measured scroll velocity
Try it yourself
0 of 8 correct

Sort each task by the tool you would reach for first.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

element.animate()

REMOTE CONTROL

The Web Animations API sits between CSS and a hand-written rAF loop. You hand the browser a choreography card: keyframes plus timing. The browser hands back an Animation object: a remote control with play(), pause(), reverse(), finish(), playbackRate, playState, currentTime, and a finished promise.

Real-life analogyA TV remote controls an animation

element.animate() gives the browser the keyframes and timing, then returns a TV remote for playback. You do not redraw every frame, but you can pause, reverse, or finish it.

In real life: Press play or pause
In JavaScript: Call play() or pause()
In real life: Skip forward or back
In JavaScript: Set currentTime or call reverse()
In real life: See whether it is playing
In JavaScript: Read playState

Where the analogy stops: A remote does not make the show. Your page still chooses the animation, reduced-motion behavior, and cleanup.

Web Animations lab: choreography card plus remote control
element.animate returns an AnimationPop out in the code editor (opens in a new tab)JavaScript
const animation = card.animate(  [    { transform: "translateX(0)", opacity: 0.6 },    { transform: "translateX(160px)", opacity: 1 },  ],  {    duration: 900,    easing: "ease-out",    iterations: 1,    direction: "normal",    fill: "forwards",  },); await animation.finished;animation.commitStyles();animation.cancel();
Animation objectidle
card
playbackRate
1x
reduced motion
no preference
Try it yourself

Click Play to create an Animation object. The buttons call methods on that object; the status reads playState, currentTime, and the finished promise.

fill: "forwards" keeps the visual end state, but many finished animations can pile up. commitStyles() writes the final computed style, then cancel() can release the animation.
The core Web Animations patternPop out in the code editor (opens in a new tab)JavaScript
const animation = card.animate(  [    { transform: "translateX(0)", opacity: 0.6 },    { transform: "translateX(160px)", opacity: 1 },  ],  {    duration: 900,    easing: "ease-out",    iterations: 1,    direction: "normal",    fill: "forwards",  },); await animation.finished;animation.commitStyles();animation.cancel();

Where you’ll use this

Real frontend work is mostly choosing the least powerful tool that still solves the problem. A menu fade might be a CSS transition from the Styles and classes lesson. A tutorial card that must pause when the learner opens a modal might use element.animate(). A canvas game or chart scrubber might use rAF, then carefully keep expensive layout reads out of the hot path.

Practical checklist
  • Animate transform and opacity when possible.
  • Measure elapsed time in rAF; never assume 16.7 ms.
  • Cancel loops and animations during cleanup.
  • Read reduced-motion preference in the browser after mount, then offer a calm path.
  • Use promises from animation.finished when later work must wait for playback.

Common misconceptions

REALITY CHECK
  • “rAF means 60 fps.” It means “before a repaint.” The display might be 60, 90, 120 Hz, variable, or throttled.
  • “Move two pixels per frame is fine.” It changes speed across displays and falls behind after slow frames.
  • “All CSS animation is free.” Transform and opacity are often cheap; layout-heavy properties can still hurt.
  • “JavaScript animation runs off the main thread.” Normal rAF callbacks and JS-driven style writes run on the main thread.
  • “fill: "forwards" is cleanup.” It preserves the visual effect; it does not remove the finished animation object.
  • “Reduced motion means no design.” It means offer less movement: instant changes, fades, or stepped progress.

Practice exercises

5 EXERCISES
Exercise 1 · Warm-upPredict elapsed-time motion

Run the code mentally. What two values print?

Starter codePop out in the code editor (opens in a new tab)JavaScript
const timestamps = [0, 16.7, 50];
let position = 0;
let previous = timestamps[0];
for (const timestamp of timestamps.slice(1)) {
  const elapsed = timestamp - previous;
  position += 0.12 * elapsed;
  console.log(position.toFixed(2));
  previous = timestamp;
}

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upPredict fixed-per-frame motion

    Now compare the fixed-per-frame version. What two values print?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    const timestamps = [0, 16.7, 50];
    let position = 0;
    for (const timestamp of timestamps.slice(1)) {
      position += 2;
      console.log(position.toFixed(2));
    }

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeFind the cleanup bug

      Find the missing line that lets the loop keep running after Reset.

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      let rafId;
      function start() {
        rafId = requestAnimationFrame(frame);
      }
      function reset() {
        position = 0;
      }
        Exercise 4 · PracticeChoose the right tool

        Write down the tool you would use for: hover underline, loading shimmer, pausable onboarding card, and canvas particles.

          Exercise 5 · ChallengeRespect reduced motion

          Sketch how a component could choose calm timing when the user prefers reduced motion.

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          const media = window.matchMedia("(prefers-reduced-motion: reduce)");
          // If media.matches, show the final state without long travel.

            Check your understanding

            7 QUESTIONS
            Animation with JavaScript quiz · 7 questionsScore: first tries count
            1. Question 1 of 7Why is requestAnimationFrame usually better than setInterval for visual animation?

              Choose an answer to see the explanation.

            2. Question 2 of 7What does this time-based motion print after a slow frame?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const timestamps = [0, 16.7, 50];
              let position = 0;
              let previous = timestamps[0];
              for (const timestamp of timestamps.slice(1)) {
                const elapsed = timestamp - previous;
                position += 0.12 * elapsed;
                console.log(position.toFixed(2));
                previous = timestamp;
              }

              Choose an answer to see the explanation.

            3. Question 3 of 7Which cubic-bezier() values are valid for CSS?

              Choose an answer to see the explanation.

            4. Question 4 of 7Which property pair is usually safest for smooth CSS animation?

              Choose an answer to see the explanation.

            5. Question 5 of 7What does element.animate(keyframes, timing) return?

              Choose an answer to see the explanation.

            6. Question 6 of 7What is the main caution with fill: "forwards"?

              Choose an answer to see the explanation.

            7. Question 7 of 7How should a page respect reduced-motion preferences?

              Choose an answer to see the explanation.

            Key takeaways

            • requestAnimationFrame schedules visual work for the next repaint and gives you a shared high-resolution timestamp.
            • Elapsed-time movement survives slow frames and high-refresh displays better than fixed-per-frame movement.
            • Timing functions shape progress; cubic Bézier x values stay in 0..1, while y values may overshoot.
            • Prefer CSS for known style interpolation, element.animate() for dynamic browser-managed playback, and rAF for live simulation.
            • Respect reduced motion, avoid autoplay, animate cheap properties, and clean up loops or finished animations.

            JavaScript animation is browser-coordinated visual change: schedule frames intentionally, measure time honestly, and choose the simplest API that gives the control you need.

            Up next: Drawing with canvas.

            CompleteFrontend Clear concepts. Working examples.