Chapter 05 of 11Part 2 · CSS
CSS keyframe animation: @keyframes and the animation property
How @keyframes and the animation property work, from fill mode to loops, and when to choose them over a transition.
10 minBy Zuzo StudioUpdated
In this chapter you will
- write an @keyframes rule and attach it with the animation shorthand
- choose a fill mode, an iteration count and a direction on purpose
- add a looping scroll cue to your hero that knows when to stop
Before you startThis builds on CSS transitions: hover, focus and entry.
A transition needs a change to happen: a hover, a class added, a value swapped. Some motion has no such moment. A loading spinner, a pulse on a new notification or a scroll cue that nudges the reader downwards has to run by itself, sometimes more than once, sometimes through several steps. That is what a CSS keyframe animation is for. You describe the steps once in an @keyframes rule, then attach them to any element with the animation property, which says how long the animation takes, how it eases, how often it repeats and what happens before and after it runs. This chapter covers each of those settings, and ends with a looping scroll cue for your hero.
01How a CSS keyframe animation is built
There are always two parts. The @keyframes rule is a named list of states, each one a percentage of the way through the animation. The animation property on an element picks a list by name and says how to play it.
@keyframes nudge {
0% { transform: translateY(0); }
50% { transform: translateY(0.5rem); }
100% { transform: translateY(0); }
}
.scroll-cue { animation: nudge 1.6s ease-in-out infinite; }
from is another way to write 0% and to is another way to write 100%. You can put as many percentages in between as you need, and two percentages that share the same state can share one block: 0%, 100% { transform: translateY(0); } does the same job as the first and last lines above.
You can also leave the ends out. MDN's @keyframes reference explains that when a rule has no 0% or 100%, the browser uses the element's existing styles for that end. This makes entrance animations short to write, because you only describe where the element comes from:
@keyframes rise {
from { opacity: 0; transform: translateY(1rem); }
}
The element animates from faded and lower to whatever its normal styles say. Change the normal styles later and the animation still lands in the right place.
Two smaller rules from the same page are worth knowing. Properties that cannot be animated are ignored inside keyframes, and so is any declaration marked !important, so you cannot force a value that way.
02The animation shorthand, piece by piece
animation is a shorthand for eight longhand properties. You will mostly write the shorthand, but you need to know what each part does to read other people's CSS and to debug your own.
| Longhand | What it sets | Default |
|---|---|---|
animation-name |
Which @keyframes rule to play |
none |
animation-duration |
How long one run takes | 0s |
animation-timing-function |
The easing between keyframes | ease |
animation-delay |
The wait before the first run | 0s |
animation-iteration-count |
How many runs, or infinite |
1 |
animation-direction |
normal, reverse, alternate or alternate-reverse |
normal |
animation-fill-mode |
Whether keyframe styles apply before and after | none |
animation-play-state |
running or paused |
running |
Most of the shorthand can come in any order, with one exception that catches everyone. MDN's animation reference says the first time value is always read as the duration and the second as the delay. So animation: rise 0.6s 0.2s lasts 0.6 seconds after a 0.2 second wait, and swapping the two numbers gives you a very different result. If you leave the duration out entirely, it is 0s and the animation finishes the moment it starts.
The easing works per step. The timing function applies between each pair of keyframes, not across the whole animation, so ease-in-out on the nudge example above slows down at the bottom of the nudge and again at the top. The curves themselves are the ones from easing and timing, and you can also set a different animation-timing-function inside an individual keyframe when one step needs its own feel.
03Fill mode: before and after the animation
By default an animation only affects the element while it is running. Before a delay ends, and after the last run, the element shows its normal styles. That is often fine for a loop, and often wrong for an entrance. animation-fill-mode changes it. MDN's animation-fill-mode reference describes the four values:
none: no keyframe styles outside the running time. The default.forwards: after the animation ends, the element keeps the values from the last keyframe it reached.backwards: during the delay, the element already shows the first keyframe.both: forwards and backwards together.
Picture the rise animation with a 0.3 second delay and no fill mode. The text appears at full opacity, vanishes when the animation starts at opacity: 0, then fades back in. That flash is the classic fill mode bug, and backwards fixes it, because the element sits in its first keyframe while it waits.
forwards needs more care. A value held by fill mode comes from the animation, and animations override normal styles, so a hover or a class change you add later may seem to do nothing. If the final keyframe matches the element's normal styles, as it does when you leave out to, you rarely need forwards at all.
04Repeats, direction and play state
animation-iteration-count takes a number, which can include fractions (1.5 plays one and a half runs), or infinite. animation-direction decides which way each run goes. alternate is the useful one for loops: the first run plays forwards, the second backwards, and so on, so a nudge or a pulse never jumps back to its start.
.scroll-cue {
animation: nudge 0.8s ease-in-out 1s 6 alternate backwards;
}
That reads: play nudge over 0.8 seconds, easing in and out, after a 1 second wait, six times, alternating direction, showing the first keyframe during the wait. With alternate you could also cut nudge down to two states, the top and the bottom, because the return trip is the same run played backwards.
animation-play-state: paused freezes an animation where it is, and running carries on from there. It is the simplest way to pause a loop on hover, or from a class your script adds when the element scrolls out of view.
05Running more than one animation
Every animation longhand accepts a comma separated list, and so does the shorthand. Each item in the list is a separate animation with its own timing:
.badge {
animation:
rise 0.5s ease-out backwards,
pulse 1.2s ease-in-out 0.5s 3;
}
The badge rises in, and once that is done it pulses three times. This is a small sequence without any JavaScript: the second animation's delay matches the first one's duration.
There is one catch. Two animations that set the same property do not add up. The animation reference shows that the one declared later overrides the earlier one for that property. Both rise and pulse above use transform, so while they overlap, only pulse would move the badge. Here they never overlap, so it works. When you need two transforms at once, a slide and a spin for example, either animate the individual translate, rotate and scale properties, which are separate from transform, or put one animation on a wrapper element and the other on its child.
Sequences much longer than this get hard to manage in CSS, because every delay depends on every earlier duration and one change means recalculating the rest. That is the point where GSAP timelines become easier, which chapter 8 covers.
06When keyframes beat transitions
Both run in CSS, both can be smooth, and both use the same easing. The difference is what starts them and how many states they have.
| Use a transition when | Use keyframes when |
|---|---|
| A state change triggers it: hover, focus, a class | It runs by itself on load, or loops |
| There are two states, before and after | There are three or more steps |
| It should reverse naturally when the state flips back | It repeats, alternates or pauses |
| Example: a button hover | Example: a spinner, a pulse, a scroll cue |
A transition reverses for free: move the pointer off a button and it eases back from wherever it was. A keyframe animation removed halfway snaps straight back to the element's normal styles. So interface feedback belongs to transitions, and motion that runs on its own schedule belongs to keyframes.
MDN's guide to using CSS animations also points out that CSS animations run well even under moderate system load, because the browser controls the whole sequence and can optimise it. That holds best when you animate transform and opacity, which is the subject of the next chapter.
.scroll-cue {
display: inline-block;
animation: cue 0.9s var(--ease-in-out) 1s 6 alternate;
}
@keyframes cue {
from { translate: 0 0; }
to { translate: 0 0.4rem; }
}
Check your understanding
3 questions
Key takeaways
- A keyframe animation is two parts: a named @keyframes rule that lists the states, and an animation property that says how to play them.
- In the animation shorthand, the first time value is the duration and the second is the delay.
- Leave out the to keyframe and the animation ends on the element's normal styles, which keeps entrances honest and easy to change.
- animation-fill-mode: backwards stops the flash before a delayed entrance; forwards holds the last keyframe and can block later style changes.
- Use transitions for state changes that should reverse, and keyframes for motion that runs, repeats or has more than two steps.
Sources
- @keyframesMDN Web Docs, checked
- animation CSS propertyMDN Web Docs, checked
- animation-fill-modeMDN Web Docs, checked
- Using CSS animationsMDN Web Docs, checked