Command Palette

Search for a command to run...

Hectal
PHASE 10Advanced ~28 min· topic 3 of 6

Topic 10.3

Keyframe Animations

In one line

@keyframes describes the steps of an animation by percentage of its time, and the animation properties attach it to an element with a duration, easing, delay, number of repeats, direction and fill mode; unlike a transition, it runs by itself, can loop and can pass through many steps.

Think of it like this

A flip book. Each page is a drawing, and when you flick through, the drawings become movement. You only draw the important pages (the ball at the top, the ball on the floor), and your eye fills in the motion. @keyframes is the list of important pages; the browser draws every frame in between. And unlike a dimmer switch that only moves when you touch it, a flip book plays as soon as you flick it, and you can flick it again and again.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Keyframe
One important moment in an animation, written as a percentage of its time with the styles at that moment.
`@keyframes`
The at-rule that names an animation and lists its keyframes.
At-rule
A CSS statement that starts with @, like @media or @keyframes.
Iteration
One complete cycle of the animation, from 0% to 100%.
Direction
Which way each cycle plays: forwards, backwards, or alternating.
Fill mode
Whether the first or last keyframe's styles also apply before the animation starts or after it ends.
Play state
Whether an animation is running or paused.
Stagger
Starting similar animations at slightly different times so they ripple, often with negative delays.

Step by step

01Describe the animation

Name it and list the keyframes. Here a ball goes up and comes back down: the same values at 0% and 100% mean it loops smoothly.

style.csswhole filecss
@keyframes bounce {
  0%, 100% { translate: 0 0; }
  50%      { translate: 0 -60px; }
}

02Attach it

Give the element the name and a duration. Without a duration (or with 0s), the animation finishes the moment it starts and you see nothing.

style.csswhole filecss
.ball {
  animation-name: bounce;
  animation-duration: 0.8s;
  animation-iteration-count: infinite;
  animation-timing-function: ease-in-out;
}
/* or in one line: */
.ball { animation: bounce 0.8s ease-in-out infinite; }

03Play it back and forth

With alternate, odd cycles go forwards and even cycles go backwards, so you only need two keyframes for a smooth back-and-forth.

style.csswhole filecss
@keyframes sway {
  from { rotate: -10deg; }
  to   { rotate: 10deg; }
}
.sign { animation: sway 1s ease-in-out infinite alternate; }

04Decide what happens before and after

A delayed fade-in shows the element fully visible during the delay, then flashes to invisible when the animation starts. backwards (or both) applies the first keyframe during the delay. forwards (or both) keeps the last keyframe after the end.

style.csswhole filecss
@keyframes fade-up {
  from { opacity: 0; translate: 0 12px; }
}
.item {
  animation: fade-up 0.4s ease-out 0.3s both;
}
Decide what happens before and afterdiagram
Rendering diagram…

05Stagger and pause

Three loading dots share one animation; negative delays start each one partway through, so they ripple instead of moving together. A class can set animation-play-state: paused to freeze them.

style.csswhole filecss
.dot { animation: blink 1.2s infinite; }
.dot:nth-child(2) { animation-delay: -0.8s; }
.dot:nth-child(3) { animation-delay: -0.4s; }
.loader.paused .dot { animation-play-state: paused; }

Try it yourself

  1. 1

    Count the events

    In the second example, change 2 to 3. Predict the events (one more animationiteration at 0.4, then animationend at 0.6) and the forwards width after the end (150px, because the third cycle runs forwards).

  2. 2

    Stop the snap-back

    In the second example, change .plain to use both, then backwards. Predict the final width each time. (both: 50px. backwards: 100px, because backwards only affects the delay.)

  3. 3

    Restyle the loader

    In the first example, change the stagger delays to -0.6s and -0.3s, and change blink to scale the dots: 50% { scale: 1.6; }. Predict how the ripple changes before you look.

Code & diagrams

Bounce, sway, spin and stagger New tab

Hover the row to pause everything. `spin` only has a `to` keyframe: the start comes from the element's own `rotate` (none, so 0).

Preview

Press Run to see the page.

Console

Nothing logged yet.
Animation events, direction and fill mode New tab

Two cycles of 0.2s: `animationiteration` fires once (between the cycles) and `animationend` at 0.4s. With `alternate`, the second cycle runs backwards, so the last frame played is the `from` frame: `forwards` keeps 50px. Without a fill mode the bar returns to its own 100px.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

animationstart grow 0
animationiteration grow 0.2
animationend grow 0.4
forwards, after the end: 50px
none, after the end: 100px
Control animations from JavaScript New tab

Each `@keyframes` attached to an element is a `CSSAnimation` object. `getAnimations()` lists them, and its `playState` follows `animation-play-state`. Two animations on one element run at the same time, each on its own clock.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

names: [ 'blink', 'wobble' ]
state: running
after adding .paused: paused
The life of one animationdiagram
Rendering diagram…

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Forget the duration

Write .ball { animation-name: bounce; } with no duration.

terminal
$ the page shows
── what you'll see ──
The ball doesn't move at all.

Break #2

A delayed fade-in that flashes

Write @keyframes fade { from { opacity: 0; } } and .card { animation: fade 0.5s 1s; }.

terminal
$ the page shows
── what you'll see ──
The card is fully visible for one second, disappears, then fades in.

Break #3

Restarting an animation by re-adding the class

On every click, run el.classList.remove("shake"); el.classList.add("shake"); to replay a shake animation.

terminal
$ the page shows
── what you'll see ──
The shake plays on the first click only. Later clicks do nothing.

Myth vs fact

Myth

animation-timing-function shapes the whole animation.

Fact

It applies to each segment between two keyframes. A 3-keyframe animation with ease eases in and out twice. Use linear on the animation and shape with keyframes, or set the easing per keyframe.

Myth

forwards keeps the 100% keyframe.

Fact

It keeps the last frame played. With reverse, or alternate and an even count, that is the 0% frame. With a fractional count like 0.5, it is the 50% point.

Myth

You need JavaScript to pause a CSS animation.

Fact

animation-play-state: paused pauses it; a :hover rule or a class can set it. JavaScript is only needed to toggle the class or to seek.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Animations and transitions sit in their own cascade origins: animation values beat all normal declarations (including inline styles), !important declarations beat animations, and transitions beat everything. That's why an animation can override a style attribute, but not an !important rule.

  • ▸

    animation-composition (Baseline 2023) controls how an animation's value combines with the underlying value: replace (default), add (append transform functions or add numbers) or accumulate. It lets a keyframe translate be added on top of an element's existing transform instead of replacing it.

  • ▸

    Unregistered custom properties animate discretely (they flip at 50%). Register them with @property --angle { syntax: "<angle>"; inherits: false; initial-value: 0deg; } and they interpolate, which unlocks animating gradient angles and colour stops (Topic 3.6, Custom Properties).

  • ▸

    Every CSS animation is a CSSAnimation in the Web Animations API: el.getAnimations() gives you pause(), play(), reverse(), playbackRate, currentTime and a finished promise. el.animate(keyframes, options) creates the same kind of animation from JavaScript (see the /javascript course). Animations also pause automatically in background tabs, and requestAnimationFrame-based animations do too.

Remember this

  1. 1

    A keyframe animation has two parts. First you describe it once with an @keyframes at-rule (a CSS rule that starts with @) and give it a name: @keyframes pulse { from { scale: 1; } to { scale: 1.2; } }. Inside, each keyframe selector is a percentage of the animation's time: from is 0%, to is 100%, and you can add any steps in between (0%, 40%, 100%). Several percentages can share a block: 0%, 100% { opacity: 1; } 50% { opacity: 0.3; }. Then you attach it to elements with animation-name: pulse and an animation-duration.

  2. 2

    The animation properties: animation-name (which @keyframes), animation-duration (the length of one cycle; the default 0s means it plays instantly and ends), animation-timing-function (easing between each pair of keyframes, not across the whole animation; a keyframe can even set its own animation-timing-function for the segment that follows it), animation-delay (wait before starting; a negative delay starts partway through, which is handy to stagger several dots), animation-iteration-count (a number like 3, a fraction like 0.5, or infinite), animation-direction, animation-fill-mode and animation-play-state (running or paused).

  3. 3

    animation-direction controls which way each cycle plays: normal (0% to 100% every time), reverse (100% to 0%), alternate (forwards, then backwards, then forwards... so a bounce never jumps back) and alternate-reverse (starts backwards). With alternate, the easing is also reversed on the backward cycles, so ease-in going forward becomes ease-out going back.

  4. 4

    animation-fill-mode decides what the element looks like outside the time the animation runs. By default (none), the keyframes only apply while playing: during the delay and after the end, the element shows its normal styles, so it often "snaps back" at the end. forwards keeps the values of the last frame played after it ends (which, with alternate and an even count, is the 0% frame!). backwards applies the first frame during the delay, so a delayed fade-in starts invisible. both does both and is a common safe choice.

  5. 5

    If a keyframe doesn't mention a property, the browser uses the element's own current value at that point. So @keyframes slide-in { from { translate: -100%; } } with no to animates to whatever translate the element normally has. Properties that can't be interpolated (like display or visibility) jump at the keyframe, halfway for discrete values. !important inside keyframes is ignored. While it runs, an animation's values override every normal declaration of the element (they sit above normal author styles in the cascade, below !important ones).

  6. 6

    The animation shorthand sets everything in one line: animation: pulse 1s ease-in-out 0.5s infinite alternate both. As with transitions, the first time is the duration and the second is the delay; the other values can come in any order, but don't name a keyframe after a keyword such as ease or infinite. Several animations are separated by commas and can run at once on one element. Animations also fire events: animationstart, animationiteration (at the end of every cycle except the last), animationend and animationcancel, each with animationName and elapsedTime.

Explain it without notes

01

What is the difference between a CSS transition and a keyframe animation?

02

Explain each value of animation-fill-mode with an example.

03

What does animation-direction: alternate do, and how does it affect easing and the end state?

04

How does the animation shorthand tell the duration from the delay, and what naming trap should you avoid?

05

How do you restart a CSS animation on demand?

Practice

01

Write a heartbeat animation that scales a .heart to 1.25 at 15% and 45% of the time and back to 1 elsewhere, looping forever every 1.2s.

02

Make three .card elements fade and slide up into view one after another (0s, 0.15s, 0.3s delays), with no flash before they start, and staying visible at the end.

03

Log "done" when a .toast finishes a 0.3s slide-out animation, then remove the toast from the page.

Trade-offs

  • ↔

    CSS animations are declarative, run without JavaScript and can be compositor-accelerated, but they are hard to make depend on runtime values; the Web Animations API (el.animate()) gives the same engine with full control from script.

  • ↔

    Infinite animations draw attention and use power on every frame; keep them small, pause them when off-screen or not needed, and turn them off for reduced-motion users (Topic 10.5).

Done when you can

  • I can write @keyframes with from/to and percentage steps.

  • I can set duration, easing, delay, iteration count and direction, and read the shorthand.

  • I can choose a fill mode to avoid flashes and snap-backs.

  • I can pause, stagger and restart animations.

  • I can listen for animationstart, animationiteration and animationend.