@keyframes – Defining the Motion
A @keyframes rule defines a named animation with waypoints (keyframes) that specify what CSS looks like at each point in the animation timeline.
/* from / to — two-step animation */
@keyframes fade-in {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}
/* Percentage waypoints — multi-step */
@keyframes bounce {
0% { transform: translateY(0); }
30% { transform: translateY(-24px); }
60% { transform: translateY(-12px); }
80% { transform: translateY(-6px); }
100% { transform: translateY(0); }
}
/* Multiple properties at once */
@keyframes hero-entrance {
0% { opacity: 0; transform: scale(0.8) translateY(40px); }
60% { opacity: 1; transform: scale(1.02) translateY(-4px); }
100% { opacity: 1; transform: scale(1) translateY(0); }
}
/* Rules can share keyframes */
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.05); opacity: 0.85; }
}
The Animation Properties
animation-name
animation-name: fade-in; /* matches @keyframes name */
animation-name: none; /* disables animation */
animation-name: spin, pulse; /* multiple animations */
animation-duration
animation-duration: 0.6s; /* how long one cycle takes */
animation-duration: 1500ms; /* same as 1.5s */
animation-timing-function
Same values as transition-timing-function: ease, linear, ease-in, ease-out, ease-in-out, cubic-bezier(), steps().
animation-delay
animation-delay: 0s; /* starts immediately */
animation-delay: 0.5s; /* waits 0.5s before starting */
animation-delay: -1s; /* starts 1s INTO the animation (skip beginning) */
animation-iteration-count
animation-iteration-count: 1; /* play once (default) */
animation-iteration-count: 3; /* play 3 times */
animation-iteration-count: infinite; /* loop forever */
animation-iteration-count: 2.5; /* 2.5 cycles — ends halfway through */
animation-direction
animation-direction: normal; /* default — plays forward */
animation-direction: reverse; /* plays backward */
animation-direction: alternate; /* forward, then backward, then forward… */
animation-direction: alternate-reverse; /* backward, then forward, then backward… */
animation-fill-mode
Controls what styles apply before and after the animation runs — one of the most misunderstood properties.
animation-fill-mode: none; /* default — no styles held */
animation-fill-mode: forwards; /* keep final keyframe styles after animation ends */
animation-fill-mode: backwards; /* apply from keyframe styles during delay period */
animation-fill-mode: both; /* apply backwards + forwards — almost always what you want */
/* Example: fade-in that stays visible after finishing */
.card {
opacity: 0; /* start invisible */
animation: fade-in 0.6s ease-out 0.2s both;
/* 'both' means:
- during delay (0.2s): apply 'from' styles (opacity: 0) — no flash of visible
- after ending: keep 'to' styles (opacity: 1) — stays visible */
}
animation-play-state
animation-play-state: running; /* default — animation plays */
animation-play-state: paused; /* pause the animation */
/* Pause on hover */
.spinner:hover { animation-play-state: paused; }
/* Controlled by JS class */
.anim-paused { animation-play-state: paused; }
animation Shorthand
/* animation: name duration timing-function delay iteration-count direction fill-mode */
animation: fade-in 0.6s ease-out 0.2s 1 normal both;
/* Most common shorthand patterns */
.spinner { animation: spin 0.8s linear infinite; }
.pulse { animation: pulse 2s ease-in-out infinite; }
.entrance { animation: fade-in 0.5s ease-out both; }
.stagger { animation: slide-up 0.4s ease-out 0.1s both; }
/* Multiple animations on one element */
.element {
animation:
fade-in 0.5s ease-out both,
float 3s ease-in-out 0.5s infinite;
}
Real-World Animation Patterns
/* 1. Loading spinner */
@keyframes spin {
to { transform: rotate(360deg); }
}
.spinner {
width: 40px; height: 40px;
border: 4px solid #e0e0e0;
border-top-color: #1572B6;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
/* 2. Skeleton loader (shimmer) */
@keyframes shimmer {
0% { background-position: -400px 0; }
100% { background-position: 400px 0; }
}
.skeleton {
background: linear-gradient(90deg, #e0e0e0 25%, #f5f5f5 50%, #e0e0e0 75%);
background-size: 800px 100%;
animation: shimmer 1.5s infinite;
border-radius: 4px;
}
/* 3. Page entrance (stagger children) */
@keyframes slide-up {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.card { animation: slide-up 0.4s ease-out both; }
.card:nth-child(1) { animation-delay: 0s; }
.card:nth-child(2) { animation-delay: 0.08s; }
.card:nth-child(3) { animation-delay: 0.16s; }
.card:nth-child(4) { animation-delay: 0.24s; }
/* 4. Attention pulse */
@keyframes attention {
0%, 100% { box-shadow: 0 0 0 0 rgba(21,114,182,0.4); }
70% { box-shadow: 0 0 0 12px rgba(21,114,182,0); }
}
.notification-dot { animation: attention 2s infinite; }
/* 5. Floating effect */
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-12px); }
}
.hero-icon { animation: float 3s ease-in-out infinite; }
/* 6. Typewriter cursor */
@keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
.cursor { animation: blink 1s step-end infinite; }
/* 7. Accessibility: always respect reduced motion */
@media (prefers-reduced-motion: reduce) {
.spinner, .skeleton, .card, .notification-dot, .hero-icon, .cursor {
animation: none;
}
}
Transition vs Animation, and the Performance Rule
Both create motion, but they answer different questions. A transition animates between two states triggered by something (hover, class change). An animation (@keyframes) runs a multi-step sequence on its own, and can loop.
/* Transition: A → B on hover */
.btn { transition: transform 0.2s; }
.btn:hover { transform: scale(1.1); }
/* Animation: multi-step, self-running, loops */
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
.dot { animation: pulse 1s infinite; }
Animate only transform and opacity
This is the one rule that separates smooth 60fps motion from janky animation. The browser can move transform and opacity on the GPU without recalculating layout. Animating width, height, top, or margin forces a reflow every frame — expensive and stuttery.
| Cheap (GPU) | Expensive (reflow) |
|---|---|
transform: translate/scale/rotate | top, left, margin |
opacity | width, height |
Need to move a box? Use transform: translateX(100px), not left: 100px.
🏋️ Practical Exercise
- Define a
@keyframesanimation. - Apply it with the
animationproperty. - Set the duration and iteration count.
- Loop it with
infinite. - Add percentage keyframes (0%, 50%, 100%).
🔥 Challenge Exercise
Create a loading spinner with @keyframes that rotates 360° infinitely, plus a separate "pulse" animation on a badge. Control timing, iteration, and easing. Explain the difference between transitions and keyframe animations and when to use each.
📋 Summary
- @keyframes name { } — defines the animation. Use
from/toor percentage waypoints. - animation-name — links element to a
@keyframesrule. - animation-duration — how long one cycle takes.
- animation-iteration-count: infinite — loops forever.
- animation-direction: alternate — forward then backward (ping-pong).
- animation-fill-mode: both — apply first keyframe during delay, keep last keyframe after end. Almost always use
both. - animation-play-state: paused — pause/resume with CSS or JS.
- Shorthand:
animation: name duration timing delay count direction fill - Stagger: use
animation-delaywith:nth-child(). - Always add
prefers-reduced-motionoverride.
Interview Questions
- What is a
@keyframesrule? - What is the difference between a transition and an animation?
- What does
animation-iteration-count: infinitedo? - How do percentage keyframes work?
- What does
animation-fill-modecontrol?
Related Topics
FAQ
The element shows its natural CSS state before the animation's first keyframe applies. Fix: set animation-fill-mode: backwards or both — this applies the from keyframe styles immediately, even during the delay period. For a fade-in animation, also set opacity: 0 on the element as a backup for browsers that don't support fill-mode.
CSS animations don't replay once complete unless looped. To restart via JavaScript: remove the class with the animation, force a reflow (read element.offsetWidth), then re-add the class. Example: el.classList.remove('animated'); el.offsetWidth; el.classList.add('animated');. The reflow forces the browser to process the removal before re-adding, restarting the animation from the beginning.
Yes, several ways: (1) Toggle a CSS class that applies the animation. (2) Set animation-play-state: paused/running via element.style.animationPlayState. (3) Use the Web Animations API (element.getAnimations()) for fine-grained control — play, pause, reverse, change speed, and listen for animation events. (4) Listen to animation events: animationstart, animationend, animationiteration.

