Kodokon kodokon.com

Transitions and animations: @keyframes, transform, timing

Animate on the compositor with transform and opacity, structure your @keyframes, and respect users' motion preferences.

10 min · 3 questions

Open this lesson in Kodokon

Before choosing what to animate, recall the rendering pipeline: layout (geometry), paint (pixels), composite (assembling layers on the GPU). Animating width or top triggers a layout on every frame, often followed by a paint: that is the recipe for jank below 60 fps. transform and opacity are handled directly by the compositor, without going back through the first two stages. The professional rule: animate only those two properties whenever possible, and simulate the rest (translate instead of top, scale instead of width).

CSS
.button {
  transform: translateY(0);
  transition:
    transform 0.2s cubic-bezier(0.2, 0.8, 0.2, 1),
    opacity 0.15s ease-out;
}

.button:hover {
  transform: translateY(-2px);
}

.button:active {
  transform: translateY(0) scale(0.97);
}
A compositor-friendly transition with a custom curve

When a transition is no longer enough (intermediate steps, looping, automatic start), move to @keyframes. Two settings make the difference between a clean animation and a visual bug: animation-fill-mode controls the state before the start (backwards applies the first keyframe during the delay) and after the end (forwards freezes the last one); both combines the two. Also note that the timing function applies between each pair of keyframes, not over the total duration: you can redefine it inside a keyframe.

CSS
@keyframes slide-in {
  from {
    transform: translateX(-100%);
    opacity: 0;
  }
  70% {
    transform: translateX(8px);
    opacity: 1;
  }
  to {
    transform: translateX(0);
  }
}

.toast {
  animation: slide-in 0.35s ease-out both;
}
An entrance with a slight overshoot

Two complementary tools worth knowing. will-change: transform promotes the element onto its own layer before the animation, avoiding a late raster; but each layer costs GPU memory, so apply it just before the animation and remove it afterwards, never as a permanent style on dozens of elements. steps(n) replaces continuous interpolation with discrete jumps: essential for sprites, typewriter-style cursors, or clocks.

CSS
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}
Respecting prefers-reduced-motion

Knowledge check

Make sure you remember the key points of this lesson.

  1. Why prefer transform: translateX(100px) over left: 100px for an animated movement?
    • transform is better supported by older browsers
    • transform is handled by the compositor without recalculating layout on every frame
    • left only works on absolutely positioned elements, transform works everywhere
    • transform accepts negative values, unlike left
  2. The toast has to stay completely transparent during the 0.5s delay, then stay visible once the animation is over. Which keyword completes the animation shorthand?
    @keyframes fade-in {
      from {
        opacity: 0;
      }
      to {
        opacity: 1;
      }
    }
    
    .toast {
      animation: fade-in 0.3s ease-out 0.5s ___;
    }
    • both
    • forwards
    • backwards
    • alternate
  3. What is the correct use of will-change?
    • Apply it globally on * to speed up the whole page
    • Add it just before an expensive animation and remove it afterwards
    • Always combine it with transform: translateZ(0)
    • Use it instead of transition for micro-interactions