A practical guide to CSS transitions and animations
When to use a transition vs a keyframe animation, the properties that matter, easing, performance and reduced motion — with patterns.
CSS gives you two ways to move things: transitions (animate a change between
two states) and animations (@keyframes, for multi-step or looping motion).
Knowing which to reach for — and how to keep it smooth — is most of the battle.
Transitions — animate a change
A transition interpolates a property when its value changes, usually via a state
like :hover, :focus or a toggled class:
.button {
background: #7c5cff;
transition: background 200ms ease, transform 200ms ease;
}
.button:hover { background: #5a3ff0; transform: translateY(-2px); }
The shorthand is property duration timing-function delay. List several,
comma-separated. A few rules:
- Name the specific properties you animate rather than
all— it’s clearer and avoids accidental transitions. - You can only transition between two defined values.
height: autoanddisplay: nonearen’t animatable (thoughtransition-behavior: allow-discreteand friends are changing that).
The hover lift card is a transition in practice.
Animations — multi-step and looping
When you need more than start/end — a loop, several stops, an entrance — use
@keyframes and the animation property:
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
.dot { animation: pulse 1.2s ease-in-out infinite; }
The animation shorthand is name duration timing-function delay iteration-count direction fill-mode play-state. The ones you’ll actually reach
for:
infinite— loop forever (spinners, pulses).direction: alternate— play forwards then backwards, so a 0→100 keyframe ping-pongs without a return step.fill-mode: forwards— keep the final frame after a one-shot animation.
See it in the shake animation, CSS spinner and loading dots.
Easing — the difference between cheap and polished
The timing function shapes the motion. ease is a fine default; linear suits
spinners; cubic-bezier() gives you custom curves. The most natural UI motion
often eases out (fast then settling): cubic-bezier(0.16, 1, 0.3, 1). Small
easing choices are what separate slick from janky.
Performance — animate transform and opacity
For 60fps, stick to the two properties the browser can animate on the GPU without re-running layout or paint:
transform(translate, scale, rotate)opacity
Animating width, height, top, left or margin forces layout on every
frame and stutters. Need to move something? Use transform: translate() instead
of top/left. This is why the snippets above animate transforms, not box
metrics.
Respect prefers-reduced-motion
Some users get motion sickness from animation. Honour their OS setting:
@media (prefers-reduced-motion: reduce) {
* { animation: none !important; transition: none !important; }
}
Better still, scope it so essential feedback still works but decorative motion stops. Every moving snippet here includes a reduced-motion block.
Transition or animation?
- Two states, triggered by interaction (hover, focus, open/close) → transition.
- Multi-step, looping, or auto-playing (spinner, pulse, entrance) →
@keyframesanimation.
Pair these with the flexbox and CSS Grid guides for layout, and grab values fast with the CSS unit converter. Browse all snippets or subscribe via RSS.