Type something to search...
CSS Transitions vs. Animations: Key Differences and Use Cases

CSS Transitions vs. Animations: Key Differences and Use Cases

You want a button's background to fade when someone hovers over it. Do you write a transition or an animation? Most developers pick one out of habit, and most of the time it works. But choosing the wrong tool leads to subtle problems: hover effects that snap back abruptly, entrance animations that can't be interrupted gracefully, or keyframes written for something a single line of transition could handle.

CSS transitions and CSS animations overlap, but they're built around different ideas. A transition describes how to move between two states when something changes. An animation describes a sequence that plays on its own terms. Once that distinction clicks, the choice becomes almost automatic.

In this post, I'll compare the two side by side, show where each one shines, and cover the newer features, like @starting-style and transition-behavior, that have shifted the balance in the last couple of years.

The One-Sentence Difference

  • A transition interpolates a property from its old value to its new value when that value changes.
  • An animation runs a predefined set of keyframes, starting whenever the animation is applied, regardless of any state change.

Transitions are reactive. Animations are scripted.

How Transitions Work

A transition needs three things: a property that can be interpolated, a duration, and a change in that property's value.

.button {
  background-color: #4f46e5;
  color: #fff;
  transition:
    background-color 200ms ease,
    transform 200ms ease;
}

.button:hover {
  background-color: #4338ca;
  transform: translateY(-2px);
}

.button:active {
  transform: translateY(0);
}

When the pointer enters the button, :hover changes background-color and transform. The browser sees the value change and animates between the old and new values over 200 milliseconds. When the pointer leaves, the value changes back, and the browser animates in reverse.

The transition longhands are:

  • transition-property: which properties to watch (all is allowed but usually a bad idea).
  • transition-duration: how long it takes.
  • transition-timing-function: the easing curve.
  • transition-delay: how long to wait before starting.
  • transition-behavior: whether discrete properties should participate (covered later).

Transitions Handle Interruption Gracefully

This is the most underrated feature of transitions. If the user hovers, then moves away halfway through, the transition reverses from its current position. Browsers even shorten the reverse duration proportionally, so a half-finished transition takes half as long to undo. You don't have to write anything to get this behaviour.

Animations don't work that way. If you swap a class that changes which animation is running, the new one starts from its own first keyframe, often causing a visible jump.

How Animations Work

An animation pairs an @keyframes rule with animation properties:

@keyframes spin {
  to {
    rotate: 1turn;
  }
}

.spinner {
  width: 2rem;
  aspect-ratio: 1;
  border: 3px solid #cbd5e1;
  border-top-color: #4f46e5;
  border-radius: 50%;
  animation: spin 800ms linear infinite;
}

The spinner starts rotating as soon as the element is rendered with that rule applied. No hover, no class change, no state is needed. It loops forever because of infinite.

Animations give you things transitions simply can't:

  • Multiple intermediate steps, like 0%, 30%, 60%, 100%.
  • Looping with animation-iteration-count.
  • Alternating direction with animation-direction: alternate.
  • Running without a state change, like on page load.
  • Pausing and resuming with animation-play-state.
  • Non-time timelines, such as scroll-driven animations via animation-timeline.

Side-by-Side Comparison

TransitionsAnimations
TriggerA property value changesThe animation is applied
StatesTwo: start and endAs many keyframes as you want
LoopingNoYes, including infinite
Runs on loadOnly with @starting-styleYes
InterruptionReverses smoothly from current pointRestarts or jumps
Direction controlImplicitly reversiblereverse, alternate
Pause and resumeNo direct controlanimation-play-state
Code neededOne declaration@keyframes plus properties
Scroll-linkedNoYes, with animation-timeline

When to Use Transitions

Reach for a transition whenever the motion connects two states that already exist in your CSS.

Hover, Focus, and Active States

.nav-link {
  color: #475569;
  text-decoration-color: transparent;
  text-underline-offset: 0.3em;
  transition:
    color 150ms ease,
    text-decoration-color 150ms ease;
}

.nav-link:hover,
.nav-link:focus-visible {
  color: #0f172a;
  text-decoration-color: currentColor;
}

Toggling UI Components

Accordions, drawers, and dropdowns that open and close based on a class or attribute are ideal. The reversibility means a user who clicks twice quickly gets smooth motion both ways.

.drawer {
  position: fixed;
  inset: 0 0 0 auto;
  width: min(22rem, 90vw);
  translate: 100% 0;
  transition: translate 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}

.drawer[data-open="true"] {
  translate: 0 0;
}

JavaScript only needs to flip data-open. CSS handles the motion.

Responding to Custom Property Changes

If a registered custom property changes, you can transition it. Here, a progress ring updates smoothly whenever the value is set from script:

@property --progress {
  syntax: "<percentage>";
  inherits: false;
  initial-value: 0%;
}

.ring {
  width: 4rem;
  aspect-ratio: 1;
  border-radius: 50%;
  background: conic-gradient(#4f46e5 var(--progress), #e2e8f0 0);
  transition: --progress 500ms ease-out;
}
document.querySelector(".ring").style.setProperty("--progress", "72%");

When to Use Animations

Reach for keyframes whenever the motion is not a simple A-to-B change, or it needs to happen without a state change.

Loading Indicators and Loops

Spinners, skeleton shimmer effects, and pulsing status dots loop continuously, so they need animations.

@keyframes shimmer {
  to {
    background-position: -200% 0;
  }
}

.skeleton {
  background: linear-gradient(90deg, #e2e8f0 25%, #f1f5f9 50%, #e2e8f0 75%);
  background-size: 200% 100%;
  animation: shimmer 1.4s linear infinite;
}

Multi-Step Sequences

An attention-grabbing "shake" on an invalid form field has several back-and-forth steps, which a transition can't express:

@keyframes shake {
  20%,
  60% {
    translate: -6px 0;
  }
  40%,
  80% {
    translate: 6px 0;
  }
}

.field[aria-invalid="true"] {
  animation: shake 360ms ease-in-out;
}

Scroll-Linked Effects

Anything tied to scroll position requires an animation with animation-timeline, because transitions have no concept of a timeline other than their own duration.

The Gray Area: Entrance and Exit Effects

For a long time, entrance effects were firmly animation territory. A transition needs a "before" value to animate from, and an element that has just been inserted into the DOM, or has just switched from display: none, has no previous style. So transitions simply didn't run.

Two newer features change that.

@starting-style

@starting-style defines the styles an element should transition from the first time it's rendered, or when it goes from display: none to visible:

.toast {
  opacity: 1;
  translate: 0 0;
  transition:
    opacity 250ms ease,
    translate 250ms ease;

  @starting-style {
    opacity: 0;
    translate: 0 1rem;
  }
}

When a .toast is added to the page, it now transitions in from transparent and slightly lowered. Before @starting-style, you'd have needed a keyframe animation or a JavaScript trick to force a reflow between two class changes.

transition-behavior: allow-discrete

display and overlay are discrete properties: they flip between values instead of interpolating. With transition-behavior: allow-discrete, you can include them in a transition, and the browser keeps the element displayed until the rest of the exit transition finishes.

Here's a popover that fades in and out, including its exit:

<button popovertarget="menu">Options</button>
<div id="menu" popover class="menu">
  <a href="/account">Account</a>
  <a href="/billing">Billing</a>
</div>
.menu {
  opacity: 0;
  scale: 0.96;
  transition:
    opacity 180ms ease,
    scale 180ms ease,
    display 180ms allow-discrete,
    overlay 180ms allow-discrete;
}

.menu:popover-open {
  opacity: 1;
  scale: 1;

  @starting-style {
    opacity: 0;
    scale: 0.96;
  }
}

The closed state is the base style. When the popover opens, @starting-style gives the entrance a starting point. When it closes, allow-discrete delays the switch to display: none until the fade finishes. Including overlay keeps the popover in the top layer until the fade finishes.

@starting-style and transition-behavior are supported in current Chromium-based browsers, Safari, and Firefox, although Firefox's support for transitioning overlay has trailed the others. In an older browser, the popover simply appears and disappears instantly, which is a perfectly acceptable fallback.

So Which Should You Use for Entrances?

  • If the element has a clear "open" and "closed" state that can be toggled back and forth, use a transition with @starting-style. You get smooth reversal if the user closes it mid-entrance.
  • If the entrance is a one-off sequence with several steps, like a hero that bounces into place, use an animation.

Performance Is the Same

A common myth is that one of them is faster than the other. Performance is determined by what you animate, not by which mechanism you use. Transitions and animations on transform, opacity, and filter can both be composited. Transitions and animations on width, height, or top both trigger layout.

The one practical difference is that infinite animations keep running even when nobody's looking at them. Stop them when the element is hidden.

Working with JavaScript

Both mechanisms fire events you can listen to:

  • Transitions fire transitionrun, transitionstart, transitionend, and transitioncancel.
  • Animations fire animationstart, animationiteration, animationend, and animationcancel.

A common pattern is to remove an element from the DOM after its exit transition:

function dismiss(toast) {
  toast.classList.add("is-leaving");
  toast.addEventListener("transitionend", () => toast.remove(), { once: true });
}

One gotcha: transitionend fires once per property. If you're transitioning opacity and translate, you'll get two events. Either check event.propertyName or use { once: true } as shown above.

Both CSS transitions and animations also show up in element.getAnimations(), which returns Web Animations API objects you can pause, reverse, or inspect.

A Quick Decision Guide

Ask these questions in order:

  1. Does it need to loop, or play more than two steps? Use an animation.
  2. Is it tied to scroll position? Use an animation with a scroll timeline.
  3. Does it respond to a state change, like hover, focus, a class, or an attribute? Use a transition.
  4. Is it an entrance for something that can also be dismissed? Use a transition with @starting-style.
  5. Is it a one-time entrance with no reverse? Either works. An animation is often simpler.

Common Pitfalls

  • Using transition: all. It transitions every property that changes, including ones you didn't intend, like color changes from theme switches. List properties explicitly.
  • Putting the transition only on the hover state. If transition is declared only inside :hover, the effect animates in but snaps out. Declare it on the base state.
  • Trying to transition height: auto. Classic transitions can't interpolate to auto. Newer browsers support interpolate-size: allow-keywords and the calc-size() function for this, but support is still limited, mainly to Chromium-based browsers, so check caniuse and keep a fallback such as transitioning grid-template-rows from 0fr to 1fr.
  • Forgetting reduced motion. Both mechanisms should respect prefers-reduced-motion. For transitions, shortening durations to near zero is usually enough.
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
  }
}

Conclusion

Transitions and animations aren't competitors. They answer different questions. A transition says "when this value changes, move smoothly to the new one", and it handles interruption and reversal for free. An animation says "play this sequence", with as many steps, loops, and directions as you need, and it can run on its own or be linked to scroll.

Use transitions for state changes: hover, focus, toggles, and, thanks to @starting-style and allow-discrete, entrances and exits of dismissible UI. Use animations for loops, multi-step choreography, and scroll-driven effects. Keep both on compositor-friendly properties, respect reduced motion, and you'll always have the right tool in hand.

Tags :
Share :

Related Posts

A Complete Guide to CSS Container Queries

A Complete Guide to CSS Container Queries

For more than a decade, responsive design meant one thing: media queries. You asked the browser how wide the viewport was and adjusted your layout ac

Continue Reading
A Comprehensive Guide to Installing Next.js

A Comprehensive Guide to Installing Next.js

Next.js has emerged as a powerful framework for building React applications, offering features like server-side rendering, static site generation, an

Continue Reading
Advanced CSS with clamp(), min(), and max(): Simplifying Dynamic Styling

Advanced CSS with clamp(), min(), and max(): Simplifying Dynamic Styling

CSS has evolved significantly, and modern tools like clamp(), min(), and max() are powerful game-changers in dynamic styling. If you’ve struggl

Continue Reading