CSS: Animations
Create multi-step animations that run automatically using @keyframes.
@keyframes Rule
@keyframes defines the steps of an animation. You give it a name and describe what the element should look like at different points, the browser fills in the movement between each step automatically.
@keyframes
@keyframes is defined separately from the element. You give it a name, then apply that name to an element with animation-name. The same keyframes can be reused on many elements.
- from, the starting state of the animation (same as 0%)
- to, the ending state of the animation (same as 100%)
- 0%, 50%, 100%, percentage steps for multi-stage animations
- The name given to @keyframes is referenced by animation-name on the element
- You can define as many steps as needed between 0% and 100%
@keyframes
The box slides in from the left using a simple from/to keyframe.
@keyframes slide-in {
from { transform: translateX(-80px); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
.box {
width: 80px;
height: 80px;
background: steelblue;
border-radius: 6px;
animation-name: slide-in;
animation-duration: 0.6s;
}animation-name and animation-duration
animation-name links an element to a set of @keyframes by name. animation-duration sets how long one cycle of the animation takes. Both are required for the animation to play.
animation-name and animation-duration
Without animation-duration, the animation plays at 0s and is invisible. These two properties are the minimum required to run an animation.
- animation-name: slide-in, applies the @keyframes named slide-in to this element
- animation-name: none, disables any running animation
- animation-duration: 0.5s, one cycle of the animation takes 0.5 seconds
- Duration uses the same s and ms units as transition-duration
- The name is case-sensitive and must match the @keyframes name exactly
animation-name + animation-duration
A circle pulses continuously using the named keyframes.
@keyframes pulse {
from { transform: scale(1); opacity: 1; }
to { transform: scale(1.25); opacity: 0.4; }
}
.pulse {
width: 60px;
height: 60px;
background: steelblue;
border-radius: 50%;
animation-name: pulse;
animation-duration: 0.8s;
animation-iteration-count: infinite;
animation-direction: alternate;
}animation-timing-function
animation-timing-function controls the speed curve of the animation, the same values as transition-timing-function. It can also be set inside a specific keyframe step to change the curve between steps.
animation-timing-function
The timing function can be placed on the element (applies to all steps) or inside a specific keyframe (applies only from that step to the next).
- ease, slow start and end, fast middle (default)
- linear, constant speed; best for continuous loops like spinners
- ease-in-out, smooth acceleration and deceleration
- steps(n), jumps between n discrete frames; useful for sprite animations
- Setting it inside a keyframe step overrides the element-level value for that step only
animation-timing-function: linear
A spinner uses linear so it rotates at a constant speed with no easing.
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #e2e8f0;
border-top-color: steelblue;
border-radius: 50%;
animation-name: spin;
animation-duration: 0.8s;
animation-timing-function: linear;
animation-iteration-count: infinite;
}animation-delay
animation-delay sets how long the browser waits before starting the animation. A negative value starts the animation immediately but partway through its cycle.
animation-delay
Use staggered delays on a list of elements to make them animate in one after another. A negative delay is useful to offset looping animations so they don't all start in sync.
- animation-delay: 0.5s, wait 500ms before the animation starts
- animation-delay: 0s, no delay (default)
- animation-delay: -0.4s, start immediately but 400ms into the animation cycle
- Stagger multiple elements by giving each a slightly larger delay
animation-delay stagger
Three dots bounce in sequence, each delayed 0.15s more than the previous.
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-16px); }
}
.row { display: flex; gap: 10px; padding: 20px; }
.dot {
width: 16px;
height: 16px;
background: steelblue;
border-radius: 50%;
animation-name: bounce;
animation-duration: 0.6s;
animation-iteration-count: infinite;
animation-timing-function: ease-in-out;
}
.d1 { animation-delay: 0s; }
.d2 { animation-delay: 0.15s; }
.d3 { animation-delay: 0.3s; }animation-iteration-count
animation-iteration-count sets how many times the animation cycles. Use infinite for looping animations, or a number to play it a fixed number of times.
animation-iteration-count
Most looping animations (spinners, pulsing indicators) use infinite. Use a specific count for one-time entrance animations or attention effects.
- animation-iteration-count: 1, plays once and stops (default)
- animation-iteration-count: 3, plays three times then stops
- animation-iteration-count: infinite, loops forever
- animation-iteration-count: 1.5, plays one and a half cycles then stops
animation-iteration-count: 3
The shake animation plays exactly three times then stops.
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-8px); }
75% { transform: translateX(8px); }
}
.shake {
display: inline-block;
background: crimson;
color: white;
padding: 10px 20px;
border-radius: 6px;
animation-name: shake;
animation-duration: 0.4s;
animation-iteration-count: 3;
animation-timing-function: ease-in-out;
}animation-direction
animation-direction controls whether the animation plays forwards, backwards, or alternates between the two on each cycle.
animation-direction
alternate is very useful for continuous looping animations, the element smoothly reverses at the end of each cycle instead of jumping back to the start.
- normal, plays from 0% to 100% each cycle (default)
- reverse, plays from 100% to 0% each cycle
- alternate, plays forward on odd cycles, backward on even cycles
- alternate-reverse, plays backward on odd cycles, forward on even cycles
- alternate avoids a sudden jump back to the start in infinite loops
animation-direction: alternate
The bar expands and contracts smoothly because alternate reverses the animation each cycle.
@keyframes grow {
from { transform: scaleX(0.3); }
to { transform: scaleX(1); }
}
.bar {
height: 20px;
background: steelblue;
border-radius: 4px;
transform-origin: left;
animation-name: grow;
animation-duration: 1s;
animation-iteration-count: infinite;
animation-direction: alternate;
animation-timing-function: ease-in-out;
}animation-fill-mode
animation-fill-mode controls what styles are applied to the element before the animation starts (during the delay) and after it finishes.
animation-fill-mode
forwards is the most commonly needed value, without it, the element snaps back to its original styles the moment the animation ends.
- none, element reverts to its original styles before and after the animation (default)
- forwards, element keeps the styles from the last keyframe (100%) after the animation ends
- backwards, element applies the styles from the first keyframe (0%) during the delay period
- both, combines forwards and backwards behaviours
- Use forwards on entrance animations so the element stays in its final position
animation-fill-mode
Both boxes animate once. The first snaps back to the start; the second stays in its final position.
@keyframes fade-up {
from { transform: translateY(20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.box {
background: steelblue;
color: white;
padding: 10px 16px;
border-radius: 4px;
margin-bottom: 10px;
opacity: 0;
animation-name: fade-up;
animation-duration: 0.6s;
animation-iteration-count: 1;
}
.none { animation-fill-mode: none; animation-delay: 0s; }
.fwd { animation-fill-mode: forwards; animation-delay: 0.3s; }animation-play-state
animation-play-state pauses or resumes an animation. Setting it to paused freezes the animation at its current frame. Resuming it with running continues from where it left off.
animation-play-state
A common pattern is to pause an animation on :hover and resume it when the user moves away, or use JavaScript to toggle paused/running dynamically.
- running, animation plays normally (default)
- paused, animation is frozen at its current frame
- Toggle via :hover to pause on hover and resume on mouse-out
- Toggle via JavaScript by changing the animation-play-state style property
animation-play-state: paused
The spinner pauses when hovered and resumes when the mouse leaves.
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.spinner {
display: inline-flex;
align-items: center;
justify-content: center;
width: 80px;
height: 80px;
border: 4px solid #e2e8f0;
border-top-color: steelblue;
border-radius: 50%;
font-size: 0.65rem;
text-align: center;
cursor: pointer;
animation-name: spin;
animation-duration: 1s;
animation-timing-function: linear;
animation-iteration-count: infinite;
animation-play-state: running;
}
.spinner:hover {
animation-play-state: paused;
}Animation Shorthand
The animation shorthand combines all animation properties in one declaration. The order is: name duration timing-function delay iteration-count direction fill-mode play-state.
animation shorthand
Only name and duration are required. The rest default to: ease, 0s delay, 1 iteration, normal direction, none fill-mode, running.
- animation: slide-in 0.5s, name and duration only
- animation: spin 1s linear infinite, name, duration, timing, iteration
- animation: fade-up 0.6s ease forwards, name, duration, timing, fill-mode
- animation: bounce 0.4s ease 0.2s 3, name, duration, timing, delay, count
- Separate multiple animations with commas: animation: spin 1s linear infinite, pulse 2s ease alternate
animation shorthand
A badge entrance animation written entirely in the shorthand.
@keyframes pop-in {
0% { transform: scale(0); opacity: 0; }
80% { transform: scale(1.15); opacity: 1; }
100% { transform: scale(1); }
}
.badge {
display: inline-block;
background: crimson;
color: white;
padding: 4px 12px;
border-radius: 999px;
font-size: 0.85rem;
font-weight: bold;
/* name | duration | timing | fill-mode */
animation: pop-in 0.4s ease-out forwards;
}Multi-step Animations
By adding percentage steps inside @keyframes, you can create animations with many distinct phases. Each percentage marks a point in the animation timeline where styles change.
Multi-step @keyframes
Use percentage steps to choreograph complex animations with pauses, direction changes, and multiple property changes at precise moments.
- 0% is the start, 100% is the end, add any percentages in between
- Multiple selectors can share a step: 0%, 100% { ... }
- The browser smoothly interpolates between each step
- animation-timing-function inside a step, controls the curve from that step to the next
- Keep multi-step animations purposeful, too many steps create visual noise
Multi-step animation
Each bar grows and shrinks through multiple keyframe steps to create a loading indicator.
@keyframes bar-anim {
0%, 100% { transform: scaleY(0.4); opacity: 0.4; }
50% { transform: scaleY(1); opacity: 1; }
}
.loader {
display: flex;
align-items: center;
gap: 6px;
height: 48px;
}
.bar {
width: 8px;
height: 100%;
background: steelblue;
border-radius: 4px;
transform-origin: bottom;
animation: bar-anim 1s ease-in-out infinite;
}
.b1 { animation-delay: 0s; }
.b2 { animation-delay: 0.15s; }
.b3 { animation-delay: 0.3s; }Multi-step colour animation
A traffic light cycles through red, amber, and green using three keyframe steps.
@keyframes traffic-light {
0%, 30% { background: crimson; }
40%, 55% { background: orange; }
65%, 95% { background: seagreen; }
100% { background: crimson; }
}
.traffic {
width: 60px;
height: 60px;
border-radius: 50%;
animation: traffic-light 3s linear infinite;
box-shadow: 0 0 12px rgba(0,0,0,0.2);
}Knowledge Check
1. Which rule defines the keyframes of a CSS animation?
2. What does animation-iteration-count: infinite do?
3. What does animation-fill-mode: forwards do?
4. Which animation-direction value makes the animation alternate between forward and backward?
5. What does animation-play-state: paused do?
6. Inside @keyframes, which keyword represents the start of the animation?