← back to section

Animation is not decoration. Good animation explains: the button "pressed", the panel "slid in", the data is "loading". Bad animation — jank and jumps — is a sign that the browser cannot fit within the 16-millisecond frame budget.

Two paths: CSS and JavaScript

CSS animations are the first choice for most tasks. The browser optimises them on its own.

/* transition — smooth change between states */
.button {
  transition: background-color 0.2s ease;
}
.button:hover { background-color: #1a3d34; }

/* animation — a sequence or infinite loop */
@keyframes pulse {
  0%, 100% { opacity: 1; }
  50%       { opacity: 0.5; }
}
.skeleton { animation: pulse 1.5s ease-in-out infinite; }

transition — when you need to smoothly move between two states on an event. animation + @keyframes — for multi-step sequences or infinite loops.

JS animations are needed when the logic depends on data — cursor position, physics, dynamic trajectory. The right tool is requestAnimationFrame (rAF), not setTimeout.

function animate(timestamp) {
  element.style.transform = `translateX(${Math.sin(timestamp / 500) * 100}px)`;
  requestAnimationFrame(animate);
}
requestAnimationFrame(animate);

requestAnimationFrame synchronises with the browser's render loop — it is called before each frame, roughly 60 times per second. setTimeout(fn, 16) gives no such guarantee: it can fall between frames or be delayed by a busy thread. For more on how the event loop manages browser tasks, see Event Loop and asynchronous code.

The key performance rule: transform and opacity

The browser paints the page in layers. When you animate transform or opacity, the browser moves a finished layer on the GPU — without recalculating geometry, without repainting. This is a composite-only operation: fast and cheap.

When you animate top, left, or width — the browser runs the full cycle: recalculates geometry (reflow), repaints, composites layers. On every frame. That is the source of jank.

/* bad: reflow on every frame */
.card { transition: top 0.3s; }
.card:hover { top: -4px; }

/* good: composite-only, no reflow */
.card { transition: transform 0.3s; }
.card:hover { transform: translateY(-4px); }

Why layout is expensive — in the article Critical rendering path.

will-change: use with care

will-change is a hint to the browser to create a GPU layer ahead of time, before the animation starts:

.animated-card { will-change: transform; }

It eliminates the small jerk at the start. But it should not be applied to everything: every layer consumes GPU memory — if there are hundreds of layers, the browser starts to slow down. Apply only to elements that are actually animated, and remove will-change after one-off animations via JavaScript.

60fps and frame budget

The display refreshes 60 times per second — a new frame every ~16.7 milliseconds. If JavaScript code takes 20 milliseconds — a frame is dropped and the animation stutters. That is exactly why heavy work is not done synchronously during animation — it is offloaded to a Web Worker or broken into chunks with requestAnimationFrame.

prefers-reduced-motion: accessibility

Some users suffer from vestibular disorders — intense animations cause them discomfort. macOS, Windows, and iOS allow enabling a "reduce motion" mode. CSS reads this flag:

@media (prefers-reduced-motion: reduce) {
  .hero-animation { animation: none; }
}

Or globally — disable all animations with animation-duration: 0.01ms and transition-duration: 0.01ms for *. For more on accessible interfaces, see Accessibility (a11y).

Common patterns

Appearing and disappearing

display: none cannot be animated — the element disappears instantly. Use opacity + visibility:

.dropdown {
  opacity: 0;
  visibility: hidden;
  transform: translateY(-8px);
  transition: opacity 0.2s ease, transform 0.2s ease, visibility 0.2s;
}
.dropdown.open {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
}

visibility: hidden removes the element from the interaction flow but allows animating via transition — unlike display: none.

Loading skeletons

A sliding gradient via transform: translateX — composite-only, does not burden the CPU:

@keyframes shimmer {
  from { transform: translateX(-100%); }
  to   { transform: translateX(100%); }
}
.skeleton-line::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent);
  animation: shimmer 1.5s ease-in-out infinite;
}

Micro-interactions

Small interface responses are done with transition on :active or a CSS class change:

.like-button { transition: transform 0.15s ease; }
.like-button:active { transform: scale(0.9); }
.like-button.liked { transform: scale(1.15); }

Animations in React

The simplest approach — CSS classes based on component state:

<div className={`notification ${visible ? 'notification--visible' : ''}`}>
  Saved
</div>
.notification {
  opacity: 0;
  transform: translateY(8px);
  transition: opacity 0.25s ease, transform 0.25s ease;
}
.notification--visible { opacity: 1; transform: translateY(0); }

This approach covers most cases. A library (such as Framer Motion) is needed for route transition animations or complex chains — it uses transform and opacity under the hood and supports prefers-reduced-motion out of the box.

In short

  • CSS transition — for event-driven state changes; animation + @keyframes — for sequences and infinite loops.
  • requestAnimationFrame — the only correct tool for JS animations; setTimeout is not synchronised with the render frame.
  • Animate transform and opacity — composite-only, no reflow. Do not animate top, left, width — that is layout on every frame.
  • will-change: transform creates a GPU layer ahead of time; it should not be applied to everything — every layer consumes GPU memory.
  • Frame budget ~16ms: heavy computation during animation causes dropped frames and jank.
  • prefers-reduced-motion: reduce — a CSS media query to disable animations for users with the corresponding system setting.
  • display: none cannot be animated — use opacity + visibility for smooth appearance and disappearance.
  • In React: CSS classes based on state cover most cases; a library is only needed for complex chains and route transitions.
  • Critical rendering path — why layout is expensive and what composite means.
  • Event Loop and asynchronous code — how requestAnimationFrame fits into the browser cycle.
  • Rendering performance — optimising React components and code-splitting.
  • Accessibility (a11y) — prefers-reduced-motion and other aspects of accessible interfaces.