Skip to content
AgileLogics
6 min read

Keyframe Animations

Prerequisites: Transitions

Transitions go from A to B when something changes. Animations run on their own, through any number of steps, and can repeat.

@keyframes spin {
  to { rotate: 1turn; }
}

@keyframes pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.4; }
}

@keyframes slide-in {
  from { opacity: 0; translate: 0 1rem; }
  to { opacity: 1; translate: 0 0; }
}

.spinner { animation: spin 1s linear infinite; }
.skeleton { animation: pulse 1.5s ease-in-out infinite; }
.toast { animation: slide-in 250ms ease-out both; }

@keyframes defines the steps (from = 0%, to = 100%); missing steps are taken from the element's current style. The animation shorthand sets:

Sub-property Example
animation-name spin
animation-duration 1s
animation-timing-function linear, ease-out, steps(8)
animation-delay 200ms (negative starts part-way through)
animation-iteration-count 3, infinite
animation-direction normal, reverse, alternate
animation-fill-mode forwards (keep the end state), backwards, both
animation-play-state running, paused

Inspecting a running animation

<style>
  @keyframes fade { from { opacity: 0; } to { opacity: 1; } }
  .a { animation: fade 2s linear 3 alternate both; }
</style>
<div class="a">Hi</div>
<script>
  const [anim] = document.querySelector(".a").getAnimations();
  console.log(anim.animationName, anim.effect.getTiming().duration + "ms",
    "iterations:", anim.effect.getTiming().iterations, anim.effect.getTiming().direction);
  anim.pause();
  anim.currentTime = 500;
  console.log("opacity at 0.5s:", getComputedStyle(document.querySelector(".a")).opacity);
  anim.currentTime = 2500;
  console.log("opacity at 2.5s:", getComputedStyle(document.querySelector(".a")).opacity);
</script>
fade 2000ms iterations: 3 alternate
opacity at 0.5s: 0.25
opacity at 2.5s: 0.75

With alternate, the second iteration runs backwards — at 2.5 seconds it's a quarter of the way back down.

Scroll-driven animations

animation-timeline: scroll() or view() links an animation's progress to scrolling instead of time — reading-progress bars, reveal-on-scroll effects — with no JavaScript:

.progress {
  position: fixed;
  inset: 0 0 auto;
  height: 4px;
  background: teal;
  transform-origin: left;
  animation: grow linear both;
  animation-timeline: scroll();
}

@keyframes grow { from { scale: 0 1; } to { scale: 1 1; } }

Supported in Chromium-based browsers (since version 115) and Safari 26; Firefox hasn't shipped it yet, so treat it as an enhancement.

Use motion with care

  • Wrap non-essential motion in @media (prefers-reduced-motion: no-preference).
  • Nothing should flash more than three times per second.
  • Infinite animations distract; stop spinners when loading ends.
  • Animate transform and opacity for smoothness.

Practice

Write a "shake" animation for an invalid form field: moving left and right a few pixels three times over 300ms, only without reduced motion.

Show solution
@keyframes shake {
  0%, 100% { translate: 0; }
  25% { translate: -4px; }
  75% { translate: 4px; }
}

@media (prefers-reduced-motion: no-preference) {
  .field-error input {
    animation: shake 100ms ease-in-out 3;
  }
}