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@mediaor@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
runningorpaused. - 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.
@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.
.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.
@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.
@keyframes fade-up {
from { opacity: 0; translate: 0 12px; }
}
.item {
animation: fade-up 0.4s ease-out 0.3s both;
}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.
.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
Count the events
In the second example, change
2to3. Predict the events (one moreanimationiterationat 0.4, thenanimationendat 0.6) and theforwardswidth after the end (150px, because the third cycle runs forwards). - 2
Stop the snap-back
In the second example, change
.plainto useboth, thenbackwards. Predict the final width each time. (both: 50px.backwards: 100px, becausebackwardsonly affects the delay.) - 3
Restyle the loader
In the first example, change the stagger delays to
-0.6sand-0.3s, and changeblinkto scale the dots:50% { scale: 1.6; }. Predict how the ripple changes before you look.
Code & diagrams
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
Console
Nothing logged yet.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
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: 100pxEach `@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
Console
Nothing logged yet.Expected console output
names: [ 'blink', 'wobble' ]
state: running
after adding .paused: pausedBreak 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.
Break #2
A delayed fade-in that flashes
Write @keyframes fade { from { opacity: 0; } } and .card { animation: fade 0.5s 1s; }.
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.
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),
!importantdeclarations beat animations, and transitions beat everything. That's why an animation can override astyleattribute, but not an!importantrule. - ▸
animation-composition(Baseline 2023) controls how an animation's value combines with the underlying value:replace(default),add(append transform functions or add numbers) oraccumulate. It lets a keyframetranslatebe 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
CSSAnimationin the Web Animations API:el.getAnimations()gives youpause(),play(),reverse(),playbackRate,currentTimeand afinishedpromise.el.animate(keyframes, options)creates the same kind of animation from JavaScript (see the/javascriptcourse). Animations also pause automatically in background tabs, andrequestAnimationFrame-based animations do too.
Remember this
- 1
A keyframe animation has two parts. First you describe it once with an
@keyframesat-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:fromis0%,tois100%, 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 withanimation-name: pulseand ananimation-duration. - 2
The animation properties:
animation-name(which@keyframes),animation-duration(the length of one cycle; the default0smeans 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 ownanimation-timing-functionfor 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 like3, a fraction like0.5, orinfinite),animation-direction,animation-fill-modeandanimation-play-state(runningorpaused). - 3
animation-directioncontrols 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) andalternate-reverse(starts backwards). Withalternate, the easing is also reversed on the backward cycles, soease-ingoing forward becomes ease-out going back. - 4
animation-fill-modedecides 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.forwardskeeps the values of the last frame played after it ends (which, withalternateand an even count, is the0%frame!).backwardsapplies the first frame during the delay, so a delayed fade-in starts invisible.bothdoes both and is a common safe choice. - 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 notoanimates to whatevertranslatethe element normally has. Properties that can't be interpolated (likedisplayorvisibility) jump at the keyframe, halfway for discrete values.!importantinside 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!importantones). - 6
The
animationshorthand 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 aseaseorinfinite. 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),animationendandanimationcancel, each withanimationNameandelapsedTime.
Explain it without notes
What is the difference between a CSS transition and a keyframe animation?
Explain each value of animation-fill-mode with an example.
What does animation-direction: alternate do, and how does it affect easing and the end state?
How does the animation shorthand tell the duration from the delay, and what naming trap should you avoid?
How do you restart a CSS animation on demand?
Practice
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.
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.
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
@keyframeswithfrom/toand 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,animationiterationandanimationend.