Animation with JavaScript
Make browser motion feel smooth with requestAnimationFrame, elapsed-time loops, cubic-bezier easing, CSS animation choices, and the Web Animations API.
- 01Drive a frame loopUse
requestAnimationFrame, its timestamp, andcancelAnimationFramewithout assuming a fixed refresh rate. - 02Shape motionExplain easing, cubic Bézier control points, overshoot, and how timing functions change perceived speed.
- 03Pick 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?”
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:
setIntervalcan 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.
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
INTERACTIVErequestAnimationFrame(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.
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.
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);- Mode
- elapsed time
- Position
- 0.0 px
- Slow frame
- not yet
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.
Switch the setting and replay. The same timestamps show why elapsed-time movement survives dropped frames better than fixed-per-frame movement.
script
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;}Timing functions & Bézier curves
PLAYGROUNDConstant 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.
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-inbegins slowly - In real life: A drawer slows near the end
- In JavaScript:
ease-outends slowly - In real life: A drawer moves evenly
- In JavaScript:
linearchanges 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.
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));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.
cubic-bezier(...) string, then use it in CSS transitions, CSS keyframes, or element.animate() timing.linear() easingModern 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
SORTCSS 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.
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.
| Tool | Use it for | Strength | Watch out |
|---|---|---|---|
| CSS transition | State changes such as hover, open/closed panels, color or transform changes | Browser can optimize many transform and opacity animations | Hard to make frame-by-frame decisions |
CSS @keyframes | Reusable decorative or loading animations with known keyframes | Declare the song once; the browser plays it | Logic and live physics do not belong here |
element.animate() | Dynamic keyframes with browser-managed playback controls | Returns an Animation remote: play, pause, reverse, finish | Still think about cleanup and fill |
requestAnimationFrame | Canvas loops, games, scroll-linked math, physics, measuring every frame | Callback gets a frame timestamp synced to the display | You own the loop and main-thread cost |
- 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
Sort each task by the tool you would reach for first.
element.animate()
REMOTE CONTROLThe 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.
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()orpause() - In real life: Skip forward or back
- In JavaScript: Set
currentTimeor callreverse() - 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.
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();- playbackRate
- 1x
- reduced motion
- no preference
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.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.
- Animate
transformandopacitywhen 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.finishedwhen 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 EXERCISESRun the code mentally. What two values print?
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;
}2.00
6.00The first movement is about 2 pixels. The slow frame covers about 4 more pixels, so the second printed position is 6.00.
Now compare the fixed-per-frame version. What two values print?
const timestamps = [0, 16.7, 50];
let position = 0;
for (const timestamp of timestamps.slice(1)) {
position += 2;
console.log(position.toFixed(2));
}2.00
4.00Two iterations add 2 pixels each. The slow frame does not move farther, which is the bug.
Find the missing line that lets the loop keep running after Reset.
let rafId;
function start() {
rafId = requestAnimationFrame(frame);
}
function reset() {
position = 0;
}function reset() {
cancelAnimationFrame(rafId);
rafId = undefined;
position = 0;
}Reset must cancel the scheduled frame; otherwise the old loop keeps writing new positions after you reset.
Write down the tool you would use for: hover underline, loading shimmer, pausable onboarding card, and canvas particles.
A hover underline is a transition, a looping shimmer is keyframes, a controlled onboarding card is element.animate(), and a canvas particle system is a rAF loop.
Sketch how a component could choose calm timing when the user prefers reduced motion.
const media = window.matchMedia("(prefers-reduced-motion: reduce)");
// If media.matches, show the final state without long travel.const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const timing = reduce ? { duration: 1 } : { duration: 600, easing: "ease-out" };
const animation = panel.animate(keyframes, timing);The important part is the branch: the same UI works, but the reduced-motion path avoids long travel.
Check your understanding
7 QUESTIONSQuestion 1 of 7Why is
requestAnimationFrameusually better thansetIntervalfor visual animation?Choose an answer to see the explanation.
Question 2 of 7What does this time-based motion print after a slow frame?
Read the code, then predictconst 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.
Question 3 of 7Which
cubic-bezier()values are valid for CSS?Choose an answer to see the explanation.
Question 4 of 7Which property pair is usually safest for smooth CSS animation?
Choose an answer to see the explanation.
Question 5 of 7What does
element.animate(keyframes, timing)return?Choose an answer to see the explanation.
Question 6 of 7What is the main caution with
fill: "forwards"?Choose an answer to see the explanation.
Question 7 of 7How should a page respect reduced-motion preferences?
Choose an answer to see the explanation.
Key takeaways
requestAnimationFrameschedules 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.