Keyframe Animations
Prerequisites: Transitions
Transitions go from A to B when something changes. Animations run on their own, through any number of steps, and can repeat.
@keyframes spin {
to { rotate: 1turn; }
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
@keyframes slide-in {
from { opacity: 0; translate: 0 1rem; }
to { opacity: 1; translate: 0 0; }
}
.spinner { animation: spin 1s linear infinite; }
.skeleton { animation: pulse 1.5s ease-in-out infinite; }
.toast { animation: slide-in 250ms ease-out both; }
@keyframes defines the steps (from = 0%, to = 100%); missing
steps are taken from the element's current style. The animation
shorthand sets:
| Sub-property | Example |
|---|---|
animation-name |
spin |
animation-duration |
1s |
animation-timing-function |
linear, ease-out, steps(8) |
animation-delay |
200ms (negative starts part-way through) |
animation-iteration-count |
3, infinite |
animation-direction |
normal, reverse, alternate |
animation-fill-mode |
forwards (keep the end state), backwards, both |
animation-play-state |
running, paused |
Inspecting a running animation
<style>
@keyframes fade { from { opacity: 0; } to { opacity: 1; } }
.a { animation: fade 2s linear 3 alternate both; }
</style>
<div class="a">Hi</div>
<script>
const [anim] = document.querySelector(".a").getAnimations();
console.log(anim.animationName, anim.effect.getTiming().duration + "ms",
"iterations:", anim.effect.getTiming().iterations, anim.effect.getTiming().direction);
anim.pause();
anim.currentTime = 500;
console.log("opacity at 0.5s:", getComputedStyle(document.querySelector(".a")).opacity);
anim.currentTime = 2500;
console.log("opacity at 2.5s:", getComputedStyle(document.querySelector(".a")).opacity);
</script>
fade 2000ms iterations: 3 alternate
opacity at 0.5s: 0.25
opacity at 2.5s: 0.75
With alternate, the second iteration runs backwards — at 2.5 seconds
it's a quarter of the way back down.
Scroll-driven animations
animation-timeline: scroll() or view() links an animation's progress
to scrolling instead of time — reading-progress bars, reveal-on-scroll
effects — with no JavaScript:
.progress {
position: fixed;
inset: 0 0 auto;
height: 4px;
background: teal;
transform-origin: left;
animation: grow linear both;
animation-timeline: scroll();
}
@keyframes grow { from { scale: 0 1; } to { scale: 1 1; } }
Supported in Chromium-based browsers (since version 115) and Safari 26; Firefox hasn't shipped it yet, so treat it as an enhancement.
Use motion with care
- Wrap non-essential motion in
@media (prefers-reduced-motion: no-preference). - Nothing should flash more than three times per second.
- Infinite animations distract; stop spinners when loading ends.
- Animate
transformandopacityfor smoothness.
Practice
Write a "shake" animation for an invalid form field: moving left and right a few pixels three times over 300ms, only without reduced motion.
Show solution
@keyframes shake {
0%, 100% { translate: 0; }
25% { translate: -4px; }
75% { translate: 4px; }
}
@media (prefers-reduced-motion: no-preference) {
.field-error input {
animation: shake 100ms ease-in-out 3;
}
}