Skip to main content

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​

transitionanimation
Startswhen the property value changeswhen the animation property is applied
Stepsfrom the old value to the new oneany number of keyframes
Repeatsnoanimation-iteration-count: infinite
Reverseby itself, when the state changes backanimation-direction: reverse or alternate
Controlduration, timing function, delay, transition-behaviorplus animation-fill-mode, animation-play-state
Eventstransitionend, transitioncancelanimationstart, animationiteration, animationend, animationcancel
  • animation-fill-mode: forwards keeps 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 to linear), linear, ease-out (good for elements that appear), cubic-bezier(), steps() for frame-by-frame animation.

What to Animate​

  • transform (translate, scale, rotate) and opacity only need compositing; colors and shadows need paint; sizes and positions need layout. See Reflow and Repaint.
  • will-change: transform hints 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.
  • display and other discrete properties are not interpolated. To animate an element appearing from display: none, use @starting-style (the values to start from) and the allow-discrete keyword in transition. Browser support: Baseline 2024.
  • height: auto cannot be transitioned directly. A common workaround is a grid wrapper animated from grid-template-rows: 0fr to 1fr, with overflow: hidden on 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, not setTimeout, see requestAnimationFrame.
  • The Web Animations API (element.animate(keyframes, options)) is the same engine as CSS animations, controlled from JavaScript: it returns an Animation with pause(), reverse() and a finished promise. Motion (formerly Framer Motion) uses it where it can and falls back to its own requestAnimationFrame engine. CSS transitions and animations are Animation objects 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 transform back to the first position, then Play by removing the transform with 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 transitionend or animationend event, or element.animate(…).finished. transitionend fires once per longhand (transition: padding gives 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 check event.target and event.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 (read element.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, width or margin instead of transform.
  • Expecting a transition from display: none to work without @starting-style.
  • Ignoring prefers-reduced-motion: large movement can cause nausea for some users. See Web Accessibility.

See Also​