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. What does animation-fill-mode: both do?
    • The animation plays forwards and then in reverse
    • The animation applies to the element and to its descendants
    • The first keyframe's styles apply during the delay, and the last keyframe's styles persist after the end
  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