CodeMIRAI
0 XP

Keyframe animations

3 min read 5 runnable examples Quiz included

A transition animates between two states when something changes. A keyframe animation runs by itself, through as many steps as you like, as many times as you like.

@keyframes

HTML
<style>
  @keyframes pulse {
    0%   { transform: scale(1);    box-shadow: 0 0 0 0 rgba(90, 72, 245, .5); }
    70%  { transform: scale(1.05); box-shadow: 0 0 0 16px rgba(90, 72, 245, 0); }
    100% { transform: scale(1);    box-shadow: 0 0 0 0 rgba(90, 72, 245, 0); }
  }
  .live { display: inline-block; margin: 20px; padding: 12px 20px; border-radius: 999px; background: #5a48f5; color: white;
          font: 600 15px system-ui; animation: pulse 1.8s ease-out infinite; }
</style>
<span class="live">● Live now</span>

@keyframes names an animation and describes its steps as percentages of the way through; from and to mean 0% and 100%. The animation property attaches it to an element.

The animation property

CSS
animation: pulse 1.8s ease-out 0s infinite normal both;
/*         name  duration timing delay count direction fill-mode */
PartValues
iteration count1 (the default), any number, or infinite
directionnormal, reverse, or alternate (forwards, then backwards)
fill modeforwards keeps the last frame afterwards; backwards shows the first frame during the delay; both does both
play staterunning or paused

A loading spinner

HTML
<style>
  @keyframes spin { to { transform: rotate(360deg); } }
  .spinner { width: 42px; height: 42px; margin: 20px; border: 4px solid #e4e1ff; border-top-color: #5a48f5;
             border-radius: 50%; animation: spin .8s linear infinite; }
</style>
<div class="spinner" role="status" aria-label="Loading"></div>

One keyframe is enough: the browser animates from the element's normal state to to.

Staggered entrances

HTML
<style>
  body { font-family: system-ui; }
  @keyframes rise {
    from { opacity: 0; transform: translateY(16px); }
    to   { opacity: 1; transform: translateY(0); }
  }
  li { list-style: none; margin: 6px 0; padding: 10px 14px; border-radius: 8px; background: #f4f2ff;
       animation: rise .5s ease-out both; }
  li:nth-child(2) { animation-delay: .1s; }
  li:nth-child(3) { animation-delay: .2s; }
  li:nth-child(4) { animation-delay: .3s; }
</style>
<ul><li>Variables</li><li>Loops</li><li>Functions</li><li>Classes</li></ul>
<button id="replay">Replay</button>
<script>
  document.querySelector("#replay").addEventListener("click", () => {
    document.querySelectorAll("li").forEach((li) => {
      li.style.animation = "none";
      li.offsetHeight;            // reading a size makes the browser notice the change
      li.style.animation = "";
    });
  });
</script>

The fill mode both matters here: it shows the from frame during each item's delay, so the later items don't flash into view before their turn.

alternate: there and back

HTML
<style>
  @keyframes float { from { transform: translateY(0); } to { transform: translateY(-14px); } }
  @keyframes hue { to { filter: hue-rotate(360deg); } }
  .rocket { display: inline-block; margin: 24px; font-size: 54px; animation: float 1.2s ease-in-out infinite alternate; }
  .rainbow { display: inline-block; width: 120px; height: 60px; vertical-align: middle; border-radius: 12px;
             background: linear-gradient(90deg, #5a48f5, #0aa9cf); animation: hue 6s linear infinite; }
</style>
<span class="rocket">🚀</span>
<div class="rainbow"></div>

alternate plays forwards and then backwards, so the rocket bobs smoothly instead of jumping back to the start.

A typing effect with steps()

HTML
<style>
  .typing { width: 22ch; white-space: nowrap; overflow: hidden; border-right: 3px solid #5a48f5;
            font: 20px "Courier New", monospace;
            animation: type 3s steps(22) infinite alternate, caret .6s step-end infinite; }
  @keyframes type { from { width: 0; } }
  @keyframes caret { 50% { border-color: transparent; } }
</style>
<p class="typing">print("Hello, world!")</p>

steps(22) jumps through 22 equal steps instead of sliding smoothly, one per character of the 22-character line. A second animation blinks the cursor.

Pausing

CSS
.ticker:hover { animation-play-state: paused; }

Respect reduced motion

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

Some people get dizzy, nauseous or distracted by motion, and switch on "reduce motion" in their system settings. This widely used snippet effectively turns animation off for them. Decorative motion should always be optional.

Finished this lesson?Mark it complete to earn +20 XP and move your progress on.