CSS Transitions and Animations
- A transition animates a property between two states when its value
changes: on
:hover, a class toggle, a new inline style. - A keyframe animation (
@keyframes+animation) runs on its own: it can have several steps, repeat, and start on page load without a state change.
Animate transform and opacity: the browser can change them on the
compositor, without layout and paint, so they stay smooth even when the main
thread is busy. Animating width, top or margin reruns layout every frame.
Respect prefers-reduced-motion.
Details
Transition vs Animation
transition | animation | |
|---|---|---|
| Starts | when the property value changes | when the animation property is applied |
| Steps | from the old value to the new one | any number of keyframes |
| Repeats | no | animation-iteration-count: infinite |
| Reverse | by itself, when the state changes back | animation-direction: reverse or alternate |
| Control | duration, timing function, delay, transition-behavior | plus animation-fill-mode, animation-play-state |
| Events | transitionend, transitioncancel | animationstart, animationiteration, animationend, animationcancel |
animation-fill-mode: forwardskeeps the values of the last keyframe played after the animation ends; otherwise the element jumps back to its own styles.- Timing functions:
ease(the default in CSS;element.animate()defaults tolinear),linear,ease-out(good for elements that appear),cubic-bezier(),steps()for frame-by-frame animation.
What to Animate
transform(translate,scale,rotate) andopacityonly need compositing; colors and shadows need paint; sizes and positions need layout. See Reflow and Repaint.will-change: transformhints that the element is about to change, so the browser can prepare a separate layer; it also creates a stacking context. Each layer costs memory: use it for a measured problem on the few elements about to animate (often set from JS right before and removed after), never on everything.displayand other discrete properties are not interpolated. To animate an element appearing fromdisplay: none, use@starting-style(the values to start from) and theallow-discretekeyword intransition. Browser support: Baseline 2024.height: autocannot be transitioned directly. A common workaround is a grid wrapper animated fromgrid-template-rows: 0frto1fr, withoverflow: hiddenon its only child.- An unregistered custom property is not interpolated: it flips at 50% in an
animation and changes at once in a transition. Register it with
@property, see CSS Custom Properties.
CSS vs JavaScript
- CSS for state changes and simple loops: declarative, can run on the compositor, needs no code.
- JavaScript when an animation depends on input or other animations:
sequencing, scrubbing, physics, gestures, values computed at runtime; a
per-frame loop uses
requestAnimationFrame, notsetTimeout, see requestAnimationFrame. - The Web Animations API (
element.animate(keyframes, options)) is the same engine as CSS animations, controlled from JavaScript: it returns anAnimationwithpause(),reverse()and afinishedpromise. Motion (formerly Framer Motion) uses it where it can and falls back to its ownrequestAnimationFrameengine. CSS transitions and animations areAnimationobjects too:element.getAnimations().
Example
.button {
transition:
transform 150ms ease-out,
background-color 150ms;
}
.button:hover {
transform: translateY(-2px);
background-color: #004fd1;
}
@keyframes fade-in {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: none;
}
}
.toast {
animation: fade-in 200ms ease-out;
}
@media (prefers-reduced-motion: reduce) {
.button,
.toast {
transition: none;
animation: none;
}
}
animation: none also drops the animation-fill-mode end state and
animationend; setting durations to 0.01ms instead keeps both (and
transitionend). A global version is in
Responsive Design.
Animating an element that appears from display: none
.popup {
display: none;
opacity: 0;
transition:
opacity 200ms,
display 200ms allow-discrete;
}
.popup.open {
display: block;
opacity: 1;
/* Starting values when the element appears */
@starting-style {
opacity: 0;
}
}
Without @starting-style the popup appears at once: an element that was not
rendered has no previous value to transition from. allow-discrete keeps
display: block until the fade-out ends.
Follow-up Questions
- How do you animate a layout change (an item moving in a list) smoothly?
FLIP: measure the First position, apply the change, measure the Last,
Invert with a
transformback to the first position, then Play by removing thetransformwith a transition. View Transitions do this natively. - Why avoid
transition: all? It animates every property that changes, including ones that should not move and ones that trigger layout. - How do you wait for an animation to finish in JavaScript? The
transitionendoranimationendevent, orelement.animate(…).finished.transitionendfires once per longhand (transition: paddinggives up to four events, one per side that changes), not at all if the transition is cancelled, and bubbles: a listener on the parent also fires for children's transitions, so checkevent.targetandevent.propertyName. - Why does a transition not run when an element is inserted and the class is
added in the same task? The browser never computed the starting style. Use
@starting-style, force a style flush between inserting and adding the class (readelement.offsetWidth) or use a double rAF. - How do you find out why an animation is janky? DevTools Performance panel: Layout or Recalculate Style entries during the animation mean a property that is not compositor-only; Rendering tab: paint flashing; Layers panel: whether the element has its own layer.
- What are View Transitions?
document.startViewTransition(update)snapshots the old and new DOM states and cross-fades them; the parts are styled with::view-transition-*pseudo-elements. See MDN.
Common Mistakes
- Animating
top,left,widthormargininstead oftransform. - Expecting a transition from
display: noneto work without@starting-style. - Ignoring
prefers-reduced-motion: large movement can cause nausea for some users. See Web Accessibility.
See Also
- How the Browser Works
- Reanimated: animations in React Native
- Responsive Design:
prefers-reduced-motion - Positioning and z-index:
transformcreates a stacking context - MDN: Using CSS transitions
- MDN: Using CSS animations