ESC

Type to search the knowledge base.

CSS Animations @keyframes

Define @keyframes, control duration, easing, iteration, and fill-mode — when to animate, what to animate, and reduced-motion defaults.

intermediate3 min read
  • css
  • animations
  • keyframes

Transitions interpolate when a property changes. Animations run a timeline you declare with @keyframes, optionally looping, reversing, or finishing in an end state without a matching “to” style on the element. Use animations for loaders, entrance choreography, and continuous motion; use transitions for hover/focus state changes.

Docs: MDN CSS animations, @keyframes, animation.

Anatomy

@keyframes pulse {
  0% {
    transform: scale(1);
    opacity: 1;
  }
  50% {
    transform: scale(1.05);
    opacity: 0.85;
  }
  100% {
    transform: scale(1);
    opacity: 1;
  }
}

.badge-live {
  animation: pulse 1.6s ease-in-out infinite;
}

Longhand you’ll actually touch:

Property Role
animation-name @keyframes name
animation-duration Length of one cycle
animation-timing-function Easing across the cycle (or per keyframe)
animation-delay Wait before start
animation-iteration-count Number or infinite
animation-direction normal | reverse | alternate …
animation-fill-mode Styles before/after (none/forwards/backwards/both)
animation-play-state running | paused

Shorthand order (common):
animation: name duration timing-function delay iteration direction fill-mode;

One-shot enter with fill-mode

@keyframes fade-up {
  from {
    opacity: 0;
    transform: translateY(0.5rem);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.toast {
  animation: fade-up 200ms ease-out both;
}

both (or forwards) keeps the end keyframe styles after the animation finishes. Without it, the element can snap back to the underlying CSS — a classic “why did my modal flash” bug.

What to animate

Prefer compositor-friendly properties: transform, opacity (and in some engines filter). Animating width, height, top, or margin triggers layout and often jank.

/* Prefer */
.drawer {
  transform: translateX(100%);
  transition: transform 200ms ease;
}
.drawer.is-open {
  transform: translateX(0);
}

/* Avoid for 60fps UI */
.drawer-slow {
  animation: widen 200ms forwards;
}
@keyframes widen {
  from { width: 0; }
  to { width: 20rem; }
}

Multiple animations

.hero-mark {
  animation:
    fade-up 400ms ease-out both,
    float 4s ease-in-out 400ms infinite;
}

Comma-separate full animation lists. Debugging: temporarily disable one name in DevTools Animations panel.

Reduced motion

@media (prefers-reduced-motion: reduce) {
  .badge-live,
  .toast,
  .hero-mark {
    animation: none;
  }
}

Or provide a calmer alternate (opacity-only, shorter, single iteration). Decorative loops should die under reduced motion; essential status may stay as a non-animated text/state change.

JS coordination

el.addEventListener("animationend", (e) => {
  if (e.animationName === "fade-up") el.remove();
});

Use animationend / animationiteration carefully with multiple animations — check animationName.

Interview out-loud

“@keyframes defines intermediate styles over a percentage timeline. I attach them with the animation shorthand, use fill-mode: forwards/both so end states stick, and prefer transform/opacity for performance. Transitions are for state changes; animations are for orchestrated or looping motion. I gate decorative motion behind prefers-reduced-motion.”

Footguns

  1. Forgetting forwards and seeing a snap-back.
  2. Infinite animations on large filtered layers — battery and GPU cost.
  3. Animating from display: none — won’t run; toggle a class after showing, or use WAAPI/@starting-style patterns carefully.
  4. Easing on stepped spinners — use steps() or linear for loaders.
  5. Relying on animation for critical info with no static equivalent.

Further reading

Related guides