Transitions and keyframe animations

CSS · Practical · 12 min

Small, purposeful motion makes interfaces feel responsive and shows users what changed.

Transitions animate a property from its old value to its new one when it changes, for example on :hover or when a class is toggled:

.button { transition: background-color 150ms ease, transform 150ms ease; }

Each entry has a property, duration, optional timing function (ease, linear, ease-in-out, cubic-bezier(), steps()) and delay. List specific properties rather than transition: all, which animates things you didn't intend and costs performance.

Not everything can transition. Discrete properties like display historically just flipped. Modern CSS allows transition-behavior: allow-discrete together with @starting-style, which defines the "before" state for elements that just appeared, so dialogs and popovers can fade in and out.

Keyframe animations run on their own, without a state change:

@keyframes spin { to { rotate: 1turn; } }
.loader { animation: spin 1s linear infinite; }

The animation shorthand sets name, duration, timing function, delay, iteration count (infinite), direction (alternate) and fill-mode (forwards keeps the final frame).

For smooth 60fps motion, animate transform and opacity: the browser can run those on the compositor without recalculating layout. Animating width, top or margin triggers layout every frame and can stutter.

Always honour prefers-reduced-motion: replace large movements with fades or nothing.

Example

.button {
  background: #4f46e5;
  transition: background-color 150ms ease, translate 150ms ease;
}
.button:hover { background: #4338ca; translate: 0 -1px; }

@keyframes fade-up {
  from { opacity: 0; translate: 0 12px; }
  to   { opacity: 1; translate: 0 0; }
}
.toast { animation: fade-up 300ms ease-out both; }

@keyframes spin { to { rotate: 1turn; } }
.spinner { animation: spin 0.8s linear infinite; }

/* popover that fades in when shown */
[popover] { opacity: 0; transition: opacity 200ms, display 200ms allow-discrete; }
[popover]:popover-open { opacity: 1; }
@starting-style { [popover]:popover-open { opacity: 0; } }

@media (prefers-reduced-motion: reduce) {
  .toast, .spinner { animation: none; }
}

Remember

  • transition animates between states; @keyframes runs a sequence
  • List specific properties, not transition: all
  • Animate transform/translate and opacity for smooth motion
  • @starting-style animates elements as they appear

Watch out for

  • Animating height or top, which triggers layout on every frame.

Check yourself

  1. Which property change is cheapest for the browser to animate?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: developer.mozilla.org/en-US/docs/Web/CSS/Guides/Transitions/Using