Skip to content
WebTricks

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: auto and display: none aren’t animatable (though transition-behavior: allow-discrete and 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) → @keyframes animation.

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.