Guides

Why your CSS transition isn't working — 4 reasons the element jumps instead of animating

Illustration: a stage curtain frozen in two positions, with a snapped pulley rope

You wrote a nice animation:

.menu { transition: all 0.3s ease; }

You click the button — the menu appears. Instantly. No 0.3 seconds. You bump it to 3 seconds — still instant.

Here's the frustrating part: CSS throws no error at all. The transition just silently doesn't run. The reason is almost always one of four — let's go from the most common.

Reason 1: you're toggling display

The most common one. The menu is hidden with display: none and becomes display: block on click.

How to check. Look at which properties change between the two states. If display is one of them — that's it.

Why it fails. To animate, the browser needs in-between values. Between "0" and "1" there's "0.5". Between "no element" and "element" there's nothing. display flips in one jump, and everything that changes with it jumps too. While the element was display: none, it had no rendered "starting" look.

How to fix it. The classic way: leave display alone and animate opacity and visibility:

.menu {
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.3s, visibility 0.3s;
}
.menu.open {
  opacity: 1;
  visibility: visible;
}

visibility can flip at the end of the animation, so the menu fades out smoothly first and only then stops catching clicks. More on the difference in display: none vs visibility: hidden.

The modern way: allow "discrete" properties to animate. Works in Chrome 117+, Firefox 129+ and Safari 17.5+:

.menu {
  display: none;
  opacity: 0;
  transition: opacity 0.3s, display 0.3s allow-discrete;
}
.menu.open { display: block; opacity: 1; }
@starting-style {
  .menu.open { opacity: 0; }  /* where to start when it appears */
}

Reason 2: you're animating to height: auto

An accordion: closed is height: 0, open is height: auto. It snaps open.

How to check. Replace auto with a number, say height: 200px. If the animation works now — auto is the problem.

Why it fails. auto isn't a number, it means "work it out yourself". The browser can't take half of "work it out yourself", so there are no in-between frames.

How to fix it. The most reliable trick today is to animate a grid row. 0fr and 1fr are numbers, and they animate:

.panel {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 0.3s;
}
.panel.open { grid-template-rows: 1fr; }
.panel > .inner { overflow: hidden; }  /* without this the content sticks out */

It works in every modern browser. There's also a new property, interpolate-size: allow-keywords, that teaches the browser to animate straight to auto. But for now only Chrome and Edge support it, so it's too early to rely on.

Don't do max-height: 9999px. The animation runs across all 9999 pixels, and closing "sticks" with a delay.

Reason 3: the element and the class arrived in the same frame

You create a toast in JavaScript, add it to the page and immediately add a class:

const toast = document.createElement("div");
toast.className = "toast";        // opacity: 0
document.body.append(toast);
toast.classList.add("visible");   // opacity: 1 — but no animation

Why it fails. The browser doesn't paint each line separately. It batches all changes and paints once. The element reached the screen already at opacity: 1 — it never saw the "0" state, so there's nothing to animate from.

How to check. Add the class inside setTimeout(..., 50). If the animation runs now — this is your case.

How to fix it. The best way is the same @starting-style from reason 1: the CSS says outright where to start. Without it — wait for the browser to paint the starting state:

document.body.append(toast);
requestAnimationFrame(() => {
  requestAnimationFrame(() => toast.classList.add("visible"));
});

Two requestAnimationFrame calls in a row guarantee the first frame has been painted. A setTimeout with a delay works too, but it's a timing lottery.

Reason 4: animations are turned off on purpose

The animation only fails for you — or only on one computer.

How to check. Search your styles for prefers-reduced-motion. Many CSS resets include a rule: "if the system asks for reduced motion — transition: none !important". With that OS setting on, every animation on the site disappears.

How to fix it. Nothing to fix: it's respect for people who get motion sick. Just check the setting on your machine. And how to build such modes properly — in the article on prefers-reduced-motion.

How do I find the cause quickly?

Open DevTools, select the element and check the Computed tab: it shows the final transition value. If your style is overridden by another rule, find who overrode it — there's a separate breakdown of why CSS isn't applying.

Is transition: all bad?

It's not an error, but it's a risk: everything starts animating, including things you didn't plan, and that's slow. List properties explicitly: transition: opacity 0.3s, transform 0.3s.

Why does it animate on hover but not when the cursor leaves?

Most likely transition is only written in the :hover rule. When the cursor leaves, that rule stops applying — and the transition with it. Move transition into the element's main rule.

Learn vibe coding — don’t just read about it

Short story-lessons, an agent simulator and daily practice — in our mobile app. Free.

Open the app
KODiQ Bot

KODiQ's AI editor. Writes about vibe coding and AI tools in plain language — every day.

All articles →