Episode 1 — Fundamentals / 1.12 — CSS Animations and Motion Design
1.12.d — Keyframe Animations
In one sentence:
@keyframeslets you define multi-step timelines that run automatically — unlike transitions (which need a trigger), keyframe animations can loop, reverse, pause, and chain without any JavaScript.
Navigation: ← 1.12.c — 3D Transforms · 1.12.e — Animation Performance →
1. The @keyframes rule
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
Percentage steps for multi-stage animations
@keyframes bounce {
0% { transform: translateY(0); }
40% { transform: translateY(-30px); }
60% { transform: translateY(-15px); }
80% { transform: translateY(-5px); }
100% { transform: translateY(0); }
}
from=0%,to=100%.- You can use any number of percentage stops.
- Properties not mentioned at a stop are interpolated from the nearest defined stops.
2. The animation shorthand
/* animation: name duration timing delay count direction fill play-state; */
animation: fadeIn 500ms ease-out 0s 1 normal forwards running;
| Sub-property | What it controls | Common values |
|---|---|---|
animation-name | Which @keyframes to use | any custom name |
animation-duration | Total cycle time | 300ms, 1s |
animation-timing-function | Easing curve | ease, linear, cubic-bezier(…) |
animation-delay | Wait before first cycle | 0s, 200ms, -500ms (negative = start partway) |
animation-iteration-count | How many cycles | 1, 3, infinite |
animation-direction | Playback direction | normal, reverse, alternate, alternate-reverse |
animation-fill-mode | Styles applied before/after | none, forwards, backwards, both |
animation-play-state | Pause/resume | running, paused |
3. animation-fill-mode — what happens before and after
Timeline: [delay] ──── [animation] ──── [after]
| Value | During delay | After final keyframe |
|---|---|---|
none | Element's own styles | Element's own styles (snaps back) |
forwards | Element's own styles | Keeps the to / 100% styles |
backwards | Applies the from / 0% styles | Element's own styles |
both | Applies from styles during delay | Keeps to styles after end |
.hero-text {
opacity: 0;
animation: fadeIn 600ms ease-out 200ms both;
/* "both" → invisible during 200ms delay (from state),
stays visible after animation ends (to state) */
}
4. animation-direction
| Value | Behavior |
|---|---|
normal | 0% → 100% every cycle |
reverse | 100% → 0% every cycle |
alternate | Odd cycles: 0% → 100%; even cycles: 100% → 0% |
alternate-reverse | Odd cycles: 100% → 0%; even cycles: 0% → 100% |
alternate is great for ping-pong effects like a pulsing glow:
.pulse {
animation: glow 1.5s ease-in-out infinite alternate;
}
@keyframes glow {
from { box-shadow: 0 0 5px rgba(59,130,246,0.5); }
to { box-shadow: 0 0 20px rgba(59,130,246,0.9); }
}
5. Infinite animations
.spinner {
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
Caution: infinite animations run the compositor continuously. On mobile, this drains battery. Pause them when off-screen or when the user requests reduced motion.
6. Multiple animations
Comma-separate them — they run simultaneously:
.entrance {
animation: fadeIn 400ms ease-out both,
slideUp 400ms ease-out both;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideUp {
from { transform: translateY(20px); }
to { transform: translateY(0); }
}
Use different delays to stagger:
.item:nth-child(1) { animation-delay: 0ms; }
.item:nth-child(2) { animation-delay: 100ms; }
.item:nth-child(3) { animation-delay: 200ms; }
7. Practical CSS-only patterns
Loading spinner
.loader {
width: 40px;
height: 40px;
border: 4px solid #e2e8f0;
border-top-color: #3b82f6;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
Fade-in on load
.fade-in {
animation: fadeIn 500ms ease-out both;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
Slide-in from left
.slide-in {
animation: slideInLeft 400ms ease-out both;
}
@keyframes slideInLeft {
from { transform: translateX(-100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
Pulse (attention)
.pulse {
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
Bounce
.bounce {
animation: bounce 0.6s ease-out;
}
@keyframes bounce {
0% { transform: translateY(0); }
30% { transform: translateY(-20px); }
50% { transform: translateY(-10px); }
70% { transform: translateY(-5px); }
100% { transform: translateY(0); }
}
8. Negative delay trick
A negative delay makes the animation start partway through its timeline:
.progress-bar {
animation: fillBar 3s linear both;
animation-delay: -1.5s; /* starts at 50% of the timeline */
}
Useful for staggered loading states where you want each item at a different phase.
9. Controlling animation with JS
element.style.animationPlayState = 'paused'; // pause
element.style.animationPlayState = 'running'; // resume
Listen for lifecycle events:
| Event | When it fires |
|---|---|
animationstart | First cycle begins (after delay) |
animationiteration | A new cycle starts (not the first) |
animationend | All cycles complete |
animationcancel | Animation removed or element hidden |
10. Key takeaways
@keyframesdefines the what;animationapplies the how (duration, easing, count, etc.).fill-mode: bothis your friend — it holds styles during delay and after completion.alternate+infinitecreates smooth back-and-forth loops.- Stagger multiple elements with
animation-delayoffsets. - Always animate
transformandopacityfor best performance.
Explain-It Challenge
Explain without notes:
- Why
animation-fill-mode: forwardsis needed to keep a fade-in visible after the animation ends. - How
animation-direction: alternatechanges whatanimation-iteration-count: infinitelooks like. - The difference between a CSS transition and a CSS keyframe animation — name at least three differences.
Navigation: ← 1.12.c — 3D Transforms · 1.12.e — Animation Performance →