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.