Keyframe animations
A transition animates between two states when something changes. A keyframe animation runs by itself, through as many steps as you like, as many times as you like.
@keyframes
<style>
@keyframes pulse {
0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(90, 72, 245, .5); }
70% { transform: scale(1.05); box-shadow: 0 0 0 16px rgba(90, 72, 245, 0); }
100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(90, 72, 245, 0); }
}
.live { display: inline-block; margin: 20px; padding: 12px 20px; border-radius: 999px; background: #5a48f5; color: white;
font: 600 15px system-ui; animation: pulse 1.8s ease-out infinite; }
</style>
<span class="live">● Live now</span>@keyframes names an animation and describes its steps as percentages of the way through; from and to mean 0% and 100%. The animation property attaches it to an element.
The animation property
animation: pulse 1.8s ease-out 0s infinite normal both;
/* name duration timing delay count direction fill-mode */| Part | Values |
|---|---|
| iteration count | 1 (the default), any number, or infinite |
| direction | normal, reverse, or alternate (forwards, then backwards) |
| fill mode | forwards keeps the last frame afterwards; backwards shows the first frame during the delay; both does both |
| play state | running or paused |
A loading spinner
<style>
@keyframes spin { to { transform: rotate(360deg); } }
.spinner { width: 42px; height: 42px; margin: 20px; border: 4px solid #e4e1ff; border-top-color: #5a48f5;
border-radius: 50%; animation: spin .8s linear infinite; }
</style>
<div class="spinner" role="status" aria-label="Loading"></div>One keyframe is enough: the browser animates from the element's normal state to to.
Staggered entrances
<style>
body { font-family: system-ui; }
@keyframes rise {
from { opacity: 0; transform: translateY(16px); }
to { opacity: 1; transform: translateY(0); }
}
li { list-style: none; margin: 6px 0; padding: 10px 14px; border-radius: 8px; background: #f4f2ff;
animation: rise .5s ease-out both; }
li:nth-child(2) { animation-delay: .1s; }
li:nth-child(3) { animation-delay: .2s; }
li:nth-child(4) { animation-delay: .3s; }
</style>
<ul><li>Variables</li><li>Loops</li><li>Functions</li><li>Classes</li></ul>
<button id="replay">Replay</button>
<script>
document.querySelector("#replay").addEventListener("click", () => {
document.querySelectorAll("li").forEach((li) => {
li.style.animation = "none";
li.offsetHeight; // reading a size makes the browser notice the change
li.style.animation = "";
});
});
</script>The fill mode both matters here: it shows the from frame during each item's delay, so the later items don't flash into view before their turn.
alternate: there and back
<style>
@keyframes float { from { transform: translateY(0); } to { transform: translateY(-14px); } }
@keyframes hue { to { filter: hue-rotate(360deg); } }
.rocket { display: inline-block; margin: 24px; font-size: 54px; animation: float 1.2s ease-in-out infinite alternate; }
.rainbow { display: inline-block; width: 120px; height: 60px; vertical-align: middle; border-radius: 12px;
background: linear-gradient(90deg, #5a48f5, #0aa9cf); animation: hue 6s linear infinite; }
</style>
<span class="rocket">🚀</span>
<div class="rainbow"></div>alternate plays forwards and then backwards, so the rocket bobs smoothly instead of jumping back to the start.
A typing effect with steps()
<style>
.typing { width: 22ch; white-space: nowrap; overflow: hidden; border-right: 3px solid #5a48f5;
font: 20px "Courier New", monospace;
animation: type 3s steps(22) infinite alternate, caret .6s step-end infinite; }
@keyframes type { from { width: 0; } }
@keyframes caret { 50% { border-color: transparent; } }
</style>
<p class="typing">print("Hello, world!")</p>steps(22) jumps through 22 equal steps instead of sliding smoothly, one per character of the 22-character line. A second animation blinks the cursor.
Pausing
.ticker:hover { animation-play-state: paused; }Respect reduced motion
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}Some people get dizzy, nauseous or distracted by motion, and switch on "reduce motion" in their system settings. This widely used snippet effectively turns animation off for them. Decorative motion should always be optional.