In short
CSS keyframe animations let you create motion that runs by itself, like loading spinners and fade-ins, using @keyframes and the animation property. What is the difference between transitions and keyframe animations? A transition needs a trigger, like a hover, and it only goes from A to B.
CSS keyframe animations let you create motion that runs by itself, like loading spinners and fade-ins, using @keyframes and the animation property.
What is the difference between transitions and keyframe animations?
A transition needs a trigger, like a hover, and it only goes from A to B. A keyframe animation can start on its own when the page loads, can have many steps, and can repeat forever.
The word keyframe comes from cartoon making. The main artist draws the important poses (the keyframes), and the in-between drawings are filled in later. In CSS, you describe the important moments, and the browser fills in the rest.
How to write @keyframes
You create an animation in two steps. First, describe it with @keyframes and give it a name. Second, attach it to an element with the animation property.
@keyframes fade-in {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.hero-title {
animation: fade-in 0.8s ease-out;
}
When the page loads, the title starts invisible and 20 pixels lower, then slides up while it fades in. from means 0% and to means 100%. For more steps, use percentages like 0%, 50% and 100%.
Animation properties and the shorthand
| Property | What it controls | Example values |
|---|---|---|
animation-name | Which @keyframes to use | fade-in |
animation-duration | How long one cycle takes | 1s |
animation-timing-function | The speed curve | ease, linear |
animation-delay | Wait before starting | 0.5s |
animation-iteration-count | How many times it plays | 3, infinite |
animation-direction | Forward, backward, or both | normal, alternate |
animation-fill-mode | Styles before and after it runs | forwards, both |
animation-play-state | Pause or play | paused, running |
The shorthand puts them on one line. A common order is: name, duration, timing function, delay, iteration count, direction, fill mode.
.notice {
animation: fade-in 0.6s ease-out 0.3s 1 normal both;
}
This plays fade-in once over 0.6 seconds, after a 0.3 second delay. The both fill mode means the element uses the first keyframe during the delay (so it is hidden) and keeps the last keyframe at the end.
Common mistake: An element "jumps back" when the animation ends. That happens because, by default, the animation's styles are removed when it finishes. Add
forwardsorbothas the fill mode to keep the final state.
Practical examples: spinner, fade-in and pulsing button
A loading spinner is a circle with one colored edge that rotates forever:
<div class="spinner" role="status" aria-label="Loading"></div>
.spinner {
width: 48px;
height: 48px;
border: 5px solid #e5e7eb;
border-top-color: #4f46e5;
border-radius: 50%;
animation: spin 0.9s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
You will see a gray ring with a purple section spinning around. We use linear so the speed stays even, and infinite so it never stops. When you only write to, the browser uses the element's normal style as the start.
A pulsing button that gently draws attention:
<button class="pulse-btn">Join for free</button>
.pulse-btn {
background: #16a34a;
color: white;
border: none;
padding: 0.9rem 1.8rem;
border-radius: 999px;
font: inherit;
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% {
transform: scale(1);
box-shadow: 0 0 0 0 rgb(22 163 74 / 0.5);
}
50% {
transform: scale(1.05);
box-shadow: 0 0 0 12px rgb(22 163 74 / 0);
}
}
The button grows slightly and sends out a soft green ring that fades away, then shrinks back, every two seconds. Writing 0%, 100% together makes the start and end the same, so the loop is seamless.
For a staggered fade-in of a list, reuse the fade-in keyframes and give each item a different animation-delay, like 0.1s, 0.2s and 0.3s, with both as the fill mode.
Animation performance and prefers-reduced-motion
For smooth animations, animate transform and opacity whenever you can. The browser can often hand these to the graphics card. Animating width, height, margin or top forces layout work on every frame and can look choppy on phones. To move something, use translate() instead of left. To grow it, use scale() instead of width.
Some people feel dizzy or sick from motion on screen. Their devices have a "reduce motion" setting, and CSS can read it with prefers-reduced-motion:
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
This turns almost all motion off for users who asked for it. We use a tiny duration instead of none, so elements still end up in their final state.
Tip: Reduced motion does not have to mean no feedback. A spinner could slow down or a fade could stay, while big movements like sliding and zooming are removed.
See MDN: Using CSS animations for every animation property.
Practice exercise
Create a "typing dots" indicator: three small circles in a row that bounce up and down one after another, like a chat app shows when someone is typing.
Hint: Use one @keyframes bounce with translateY(), infinite iteration, and give the second and third dot an animation-delay of 0.15s and 0.3s.
Key takeaways
@keyframesdescribes the steps;animationattaches them to an element.- Use
from/toor percentages for the steps. infiniterepeats forever, andforwardsorbothkeeps the final state.- Animate
transformandopacityfor the smoothest results. - Respect
prefers-reduced-motionto protect users who are sensitive to motion.
Frequently asked questions
Can I pause a CSS animation on hover?
Yes. Add animation-play-state: paused; in a :hover rule. The animation freezes where it is and continues when the mouse leaves.
Should I use CSS animations or JavaScript animations?
For simple effects like spinners, fades and hover motion, CSS is easier and fast. JavaScript is better when the animation depends on user input, like dragging, or needs complex control.
Why is my animation not running?
Check that the name in animation exactly matches the @keyframes name, that you set a duration (the default is 0s), and that your device does not have reduced motion turned on.