Beyond A to B
A transition goes from one state to another, and only when something triggers it. A keyframe animation can do more: it can start by itself, pass through several stages, loop and reverse. Spinners, pulsing notifications, text that slides in on load: all keyframe animations.
There are two parts: define the steps with @keyframes, and attach them with the animation property.
Step 1: @keyframes
@keyframes fade-up {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}You choose the name (fade-up). from and to are the start and end; for more steps, use percentages:
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.15); }
100% { transform: scale(1); }
}Step 2: attach it
<style>
@keyframes fade-up {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.15); }
}
.enter { animation: fade-up 0.6s ease-out both; padding: 16px; background: #e0f2fe; border-radius: 8px; margin-bottom: 16px; }
.dot { width: 24px; height: 24px; border-radius: 50%; background: #ef4444; animation: pulse 1.2s ease-in-out infinite; }
</style>
<div class="enter">I fade up when the page loads.</div>
<div class="dot"></div>(Open the editor's run button again to replay the entrance.) The shorthand is:
animation: name duration timing-function delay iteration-count direction fill-mode;The animation properties
animation-name: which@keyframes.animation-duration: how long one cycle takes (required).animation-timing-function: pacing (ease,linear,ease-in-out,steps(8)).animation-delay: wait before starting; negative values start part-way through, which is handy to offset a group of items.animation-iteration-count: a number orinfinite.animation-direction:normal,reverse,alternate(forward then back),alternate-reverse.animation-fill-mode: what the element looks like before and after:none: returns to its normal styles afterwards.forwards: keeps the final keyframe.backwards: applies the first keyframe during the delay.both: both of the above. Good for entrances.
animation-play-state:runningorpaused, which you can toggle on hover.
A loading spinner
<style>
@keyframes spin { to { transform: rotate(360deg); } }
.spinner {
width: 40px; height: 40px;
border: 5px solid #e2e8f0;
border-top-color: #4f46e5;
border-radius: 50%;
animation: spin 0.9s linear infinite;
}
</style>
<div class="spinner" role="status" aria-label="Loading"></div>Spinners use linear timing so the speed stays constant. The role="status" and aria-label tell screen readers something is loading.
Staggered entrances
Delay each item a bit more than the last, using a custom property (the next lessons explain these) or separate rules:
.item { animation: fade-up .5s ease-out both; }
.item:nth-child(2) { animation-delay: .1s; }
.item:nth-child(3) { animation-delay: .2s; }both makes each item stay invisible (first keyframe) during its delay, so nothing flashes in early.
Performance
As with transitions, animate transform and opacity. Animating width, height, top or box-shadow forces layout or paint work on every frame. For infinite animations, keep the animated area small and stop them when off-screen if you can. A hundred pulsing elements drain a phone battery.
Being kind: motion and accessibility
Animation can delight and can harm.
- Reduced motion. Many people turn on "reduce motion" because large movement makes them dizzy or sick. Respect it:
@media (prefers-reduced-motion: reduce) {
.enter, .dot, .spinner { animation: none; }
}Better, swap movement for a simple fade rather than removing all feedback.
- No flashing. Nothing should flash more than three times in a second; it can trigger seizures.
- Do not auto-play distracting loops next to text people are trying to read, and offer a way to pause anything that moves for more than five seconds.
- Do not hide content behind animation. If the animation fails, the content must still be visible.
Debugging
Browser developer tools have an Animations panel (Chrome/Edge) to slow down, replay and scrub animations. Slowing an animation to 10 percent speed reveals problems you would never see in real time.
When to use which
| You want | Use |
|---|---|
| A hover or focus effect | Transition |
| A one-off entrance on load | Animation with both |
| A loop (spinner, pulse) | Animation with infinite |
| Scroll-linked effects | Newer scroll-driven animation CSS (check browser support) |
| Complex sequences | JavaScript or an animation library, only if CSS is not enough |
Common mistakes
- Forgetting
animation-duration, so nothing plays (default is 0s). - A typo in the
@keyframesname. - No fill-mode, so an element snaps back after finishing.
- Animating layout properties.
- No reduced-motion alternative.
- Animations that never stop next to body text.
Exercise
Make a notification badge that pulses gently forever, and a card that slides in from the left on load and stays in place. Add reduced-motion protection for both.
Show one possible answer
<style>
@keyframes slide-in { from { opacity: 0; transform: translateX(-30px); } to { opacity: 1; transform: none; } }
@keyframes pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.12); } }
.card { animation: slide-in .5s ease-out both; padding: 16px; background: #dcfce7; border-radius: 10px; }
.badge { display: inline-block; padding: 2px 10px; background: #dc2626; color: white; border-radius: 999px; animation: pulse 1.6s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) { .card, .badge { animation: none; } }
</style>
<div class="card">New message <span class="badge">3</span></div>Key points
- A keyframe animation describes steps with @keyframes, then attaches to an element with the animation property.
- Unlike a transition, an animation can run on its own, repeat, reverse and pass through several states.
- animation-iteration-count, direction, timing-function and fill-mode control how it plays and what happens at the end.
- Animate transform and opacity for smoothness and keep movement small and purposeful.
- Always provide a prefers-reduced-motion alternative, and avoid anything that flashes.