1Introduction to Browser Animation
▶
Browser animation is one of the most visible and impactful aspects of modern web development. Every time a menu slides open, a button pulses to draw attention, or a loading spinner keeps a user informed, animation is at work. Understanding how the browser produces these effects — and why some approaches work better than others — is the essential foundation for building interfaces that feel polished, responsive, and alive. This topic establishes that foundation by explaining what animation fundamentally is at the browser level, why it matters for users, the two major technical approaches available to developers, and the performance principles that determine whether an animation enhances or harms an experience.
What Browser Animation Is
At its core, every animation can be reduced to three components: a start state, an end state, and a transition that carries the subject from one to the other over a defined period of time. The start state might be a button with a white background; the end state might be the same button with a blue background; the transition is the series of intermediate color values the browser calculates and paints in between. Even if you only define the two endpoints, the browser fills in every intermediate step automatically.
The reason this works is that a browser does not display a static image. It repaints the screen continuously — typically at 60 times per second on most displays, and up to 120 or 144 times per second on high-refresh-rate screens. Each one of these paints is called a frame. Smooth animation depends entirely on synchronizing visual changes with these frame boundaries. If the browser updates a value exactly once per second rather than once per frame, the result looks like a jarring jump rather than a smooth motion. If the browser misses a frame deadline because too much computation is happening at once, users perceive the result as a stutter or "jank."
It is worth recognizing that the threshold for what counts as animation is lower than most people assume. A button that changes color when hovered, a paragraph that fades in as it enters the viewport, a tooltip that appears without any transition — all of these involve the browser calculating and rendering visual changes. Even the "instant" appearance of a tooltip requires at least one repaint. Understanding this spectrum, from instantaneous state changes to complex multi-stage motion sequences, helps developers make deliberate choices about when and how to animate.
Why Animation Matters for User Experience
Animation is not decoration. When used thoughtfully it serves clear communicative functions that make interfaces easier to understand and more pleasant to use. When used carelessly it creates noise, confusion, and frustration. The distinction lies in whether the motion carries meaning.
One of the most important roles animation plays is communicating state changes. A loading spinner is a canonical example: without it, a user who clicks a submit button and sees nothing happen cannot tell whether the system is working, whether the click was registered, or whether something went wrong. The spinner replaces that uncertainty with a clear signal — "the system is processing your request." Similarly, a progress bar communicates not just that work is happening, but roughly how far along it is. These animations convey information that would otherwise require text to explain.
Animation also helps users understand spatial and hierarchical relationships within an interface. Consider a settings panel that, when opened, slides in from the right side of the screen. This motion tells the user something that a simple "appear" cannot: the panel is a secondary layer that exists spatially beside the main content, and closing it will return them to where they were. A modal dialog that scales up from the center of the screen communicates that it is a focused overlay above the current content. These spatial metaphors reduce cognitive load because they align with users' intuitive mental models of three-dimensional space.
A third function is confirming interactions. When a user taps a button and the button immediately depresses or flashes, they receive tactile-like feedback that their action was received. Without this feedback, users often tap again — sometimes causing duplicate submissions or other unintended consequences. A subtle scale-down effect on tap, a momentary color shift, or a ripple radiating outward from the point of contact all serve this same purpose: they close the feedback loop between the user's action and the system's acknowledgment.
There is also an aesthetic dimension. Interfaces that animate well feel higher quality. Motion that follows natural easing curves — accelerating at the start and decelerating at the end, mimicking the physics of real objects — feels more satisfying than robotic linear movement. This is why design systems from major technology companies invest significant effort in defining animation standards, timing values, and easing functions.
The Two Primary Approaches to Animation
Web developers have two broad toolkits for producing animation: CSS-based animation and JavaScript-driven animation. These are not mutually exclusive — they can be combined — but understanding each one's strengths and limits is essential for choosing the right tool for a given situation.
CSS-based animation is declarative. Rather than writing instructions that say "move this element 10 pixels every millisecond," you write stylesheet rules that describe the desired states and let the browser figure out how to get from one to the other. This approach covers two distinct mechanisms: transitions and keyframe animations. A CSS transition is the simpler of the two — you declare which properties should animate, over what duration, and with what timing function, and the browser automatically animates those properties whenever their values change. A CSS keyframe animation is more expressive — using the @keyframes rule you define named animation sequences with as many intermediate steps as you need, and then apply that sequence to any element. The key structural advantage of CSS animation is that the browser can often hand it off to the compositor thread, a separate processing pathway that handles painting without blocking JavaScript execution on the main thread. This means a CSS animation can continue running smoothly even while JavaScript is doing heavy work.
JavaScript-driven animation is imperative. You write code that runs on every frame, calculates new values, and applies them to elements directly. The critical API for this is requestAnimationFrame, which schedules a callback to run immediately before the browser's next repaint. This approach is necessary whenever animation logic depends on factors that cannot be expressed in a stylesheet — for example, an animation that responds to the user's cursor position in real time, a physics simulation that calculates velocity and acceleration, or an animation that starts, pauses, or reverses based on data received from a server. JavaScript animation libraries such as GSAP (GreenSock Animation Platform) and Anime.js are built on this foundation, adding higher-level abstractions, timeline sequencing, and easing utilities on top of the raw requestAnimationFrame loop.
Choosing between the two approaches depends on several factors:
- Complexity: Simple hover effects and entrance animations are well-served by CSS. Multi-stage, data-driven, or physically simulated animations often require JavaScript.
- Interactivity: If the animation must respond to user input in real time — such as a drag gesture or a cursor-following effect — JavaScript is necessary.
- Performance: CSS transitions and animations on compositor-friendly properties (particularly
transformandopacity) are typically the most performant choice. JavaScript can match this performance when written carefully, but it requires more deliberate effort. - Maintainability: CSS keeps animation logic in stylesheets, which some teams find easier to manage. JavaScript centralizes animation logic in code, which can be easier when animations are tightly coupled to application state.
CSS-Based Animation: A Closer Look
CSS transitions are triggered automatically when a property changes value. The following example makes a button smoothly change its background color over 300 milliseconds whenever the user hovers over it:
button {
background-color: white;
transition: background-color 300ms ease-in-out;
}
button:hover {
background-color: steelblue;
}
No JavaScript is required. The browser detects that the computed value of background-color changed when the :hover state was applied, sees the transition declaration, and automatically generates the intermediate frames. The ease-in-out timing function means the color change starts slowly, accelerates through the middle, and slows again at the end — a natural-feeling curve.
CSS keyframe animations are more powerful. The following example creates a pulsing effect that could be used to draw attention to a notification badge:
@keyframes pulse {
0% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.15); opacity: 0.85; }
100% { transform: scale(1); opacity: 1; }
}
.badge {
animation: pulse 1.2s ease-in-out infinite;
}
The @keyframes rule defines the full timeline of the animation at named percentage points. The animation property applies this timeline to any element with the class badge, running it indefinitely with a duration of 1.2 seconds. Notice that both transform and opacity are being animated — these are specifically the two properties that browsers can animate on the compositor thread without triggering expensive layout or paint recalculation, making this kind of animation exceptionally smooth.
JavaScript-Driven Animation: A Closer Look
requestAnimationFrame is the foundation of all JavaScript animation. The browser calls the provided callback once per frame, immediately before painting. The callback receives a timestamp — the number of milliseconds since the page first loaded — which you use to calculate how much time has elapsed and how far along the animation should be:
const element = document.querySelector('.box');
const duration = 600; // milliseconds
let startTime = null;
function animate(timestamp) {
if (startTime === null) startTime = timestamp;
const elapsed = timestamp - startTime;
const progress = Math.min(elapsed / duration, 1); // clamp to [0, 1]
// Move the box 200px to the right over 600ms
element.style.transform = `translateX(${progress * 200}px)`;
if (progress < 1) {
requestAnimationFrame(animate);
}
}
requestAnimationFrame(animate);
This pattern is the basis for virtually all JavaScript animation. The key insight is that animate calls requestAnimationFrame(animate) at the end of each frame, scheduling itself for the next one, forming a loop that continues until the animation is complete. Because requestAnimationFrame is tied to the browser's repaint cycle rather than to a fixed time interval (as setInterval would be), it automatically adjusts to the display's refresh rate and pauses when the tab is hidden — saving battery and preventing runaway loops.
JavaScript animation becomes especially powerful when behavior must be dynamic. For example, an animation that tracks the mouse cursor requires per-frame logic that CSS simply cannot express:
const cursor = { x: 0, y: 0 };
const follower = document.querySelector('.follower');
window.addEventListener('mousemove', (e) => {
cursor.x = e.clientX;
cursor.y = e.clientY;
});
function followCursor() {
// Smoothly interpolate toward the cursor position
const rect = follower.getBoundingClientRect();
const currentX = rect.left + rect.width / 2;
const currentY = rect.top + rect.height / 2;
const dx = cursor.x - currentX;
const dy = cursor.y - currentY;
follower.style.transform = `translate(
${parseFloat(follower.dataset.x || 0) + dx * 0.1}px,
${parseFloat(follower.dataset.y || 0) + dy * 0.1}px
)`;
requestAnimationFrame(followCursor);
}
requestAnimationFrame(followCursor);
Here the animation never ends; it runs continuously, every frame, smoothly moving an element toward wherever the mouse currently is. This kind of effect is simply not possible in CSS alone.
Performance Fundamentals for Smooth Animation
Not all animated CSS properties are created equal. The browser's rendering pipeline involves several stages, and animated properties that trigger earlier, more expensive stages are costlier to animate. The pipeline stages are roughly:
| Stage | What it does | Triggered by changing… | Cost |
|---|---|---|---|
| Layout (Reflow) | Recalculates the size and position of elements | width, height, margin, padding, top, left, font-size |
Highest — affects the entire document tree |
| Paint | Fills in pixels for each element | color, background-color, border-color, box-shadow |
Medium — scoped to affected layers |
| Composite | Combines painted layers and sends to the screen | transform, opacity |
Lowest — happens on the compositor thread |
The practical implication is direct: wherever possible, animate transform and opacity. If you need to move an element, use transform: translate() rather than animating left or top. If you need to resize it visually, use transform: scale() rather than animating width and height. These compositor-only properties bypass layout and paint entirely, keeping the animation on a separate thread that runs independently of JavaScript execution.
Another important tool is the will-change CSS property. By declaring will-change: transform on an element before its animation begins, you hint to the browser that this element is about to move, allowing it to promote the element to its own compositor layer in advance:
.animated-card {
will-change: transform, opacity;
}
This promotion has a cost — compositor layers consume GPU memory — so will-change should be applied judiciously, added when animation is imminent and removed afterward, rather than applied globally as a blanket optimization. Overusing it can actually degrade performance by forcing the browser to maintain too many layers simultaneously.
In JavaScript-driven animation, the same property rules apply. Using requestAnimationFrame correctly and animating only transform and opacity via JavaScript produces results just as performant as CSS animation. Conversely, reading layout properties (such as offsetWidth or getBoundingClientRect) and then immediately writing style changes within the same frame causes forced synchronous layout — the browser is forced to recalculate layout mid-frame before it was ready to, stalling the pipeline. This is a common source of jank in JavaScript animations and is avoided by separating reads and writes: gather all measurements before making any changes, not interleaved with them.
The overarching principle is to keep animation work off the main thread. The main thread in a browser handles JavaScript execution, style calculation, layout, and paint. When any of these tasks takes too long — more than approximately 16 milliseconds at 60fps — a frame is dropped and users see a stutter. By using CSS animations on compositor-friendly properties or by writing JavaScript animation that only touches compositor-layer properties, developers push animation work to the compositor thread, which runs independently and is far less likely to miss its deadlines.