CSS Animations Overview
CSS animations let an element move, change, or cycle through styles automatically, without waiting for anything to happen. Where a transition only plays when a property actually changes value — usually triggered by :hover, :focus, or a class toggle from JavaScript — an animation defines its own timeline of steps and runs it on its own, on page load, in a loop, forever if you want. A spinning loading indicator or a pulsing notification badge are animations, not transitions, because nothing external is "triggering" a state change.
Transitions vs. animations
Transition | Animation | |
|---|---|---|
Trigger | Needs a state change (:hover, class toggle, JS) | Runs automatically once applied — no trigger needed |
Steps | Only two states: start and end | Any number of steps via @keyframes (0%, 25%, 50%...) |
Looping | Plays once per trigger | Can repeat infinitely with animation-iteration-count |
Direction control | Reverses automatically if the trigger reverses | Explicit control via animation-direction (normal, reverse, alternate) |
Typical use | Hover states, focus rings, menu open/close | Loaders, attention-grabbers, ambient motion, onboarding |
Two-part structure
Every CSS animation has two halves that work together:
@keyframes — defines what happens: a named sequence of styles at points along the timeline (0%, 50%, 100%, or from/to).
animation- properties* — define how it plays on a given element: which keyframes to use, how long, how many times, what easing.
A simple worked example
A notification dot that pulses continuously, drawing the eye without any interaction:
@keyframes pulse {
0% {
transform: scale(1);
opacity: 1;
}
70% {
transform: scale(1.6);
opacity: 0.4;
}
100% {
transform: scale(1);
opacity: 1;
}
}
.notification-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: #e74c3c;
animation-name: pulse;
animation-duration: 1.6s;
animation-timing-function: ease-in-out;
animation-iteration-count: infinite;
/* shorthand equivalent of the four lines above: */
/* animation: pulse 1.6s ease-in-out infinite; */
}Nothing on the page needs to be hovered or clicked — the moment this element renders, animation-name links it to the pulse keyframes and the browser starts cycling through them on its own, forever, because of infinite.
A second example — a bouncing loader
@keyframes bounce {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-16px);
}
}
.loader-dot {
width: 12px;
height: 12px;
border-radius: 50%;
background: #0066cc;
animation: bounce 0.6s ease-in-out infinite;
}
/* Stagger three dots so they bounce in sequence */
.loader-dot:nth-child(2) { animation-delay: 0.15s; }
.loader-dot:nth-child(3) { animation-delay: 0.3s; }This combines an animation with animation-delay and :nth-child() to create the classic three-dot "typing" loader — each dot runs the exact same keyframes, just offset in time.