CSS transitions vs keyframes

Sep 18, 2026 · Motion · 3 min read

Click the dark mode toggle on a lot of websites and watch closely. The page does not switch themes, it ripples. Buttons fade over 150ms, cards over 200ms, text snaps instantly, and for a moment you are looking at two themes at once. It is a small bug, but it comes from something worth understanding properly: how CSS decides what moves, and for how long.

CSS gives you two ways to animate, and they behave very differently when a person changes their mind halfway through. Picking the wrong one is behind a lot of motion that feels janky even when every individual value is right.

Transitions can reverse, keyframes cannot

A transition animates from wherever the property is right now toward the new target. If the target changes mid-flight, it simply turns around. A keyframe animation runs its script from the start; interrupt it and it jumps.

Not recommended
Recommended

So the rule is simple. Anything a person can toggle (open and close, hover, expand, select) should use transitions. Keyframes are for one-shot sequences nobody interrupts: a loading spinner, a success burst, an attention pulse.

Never write transition: all

transition: all animates every property that changes, including the ones you did not think about: a width that changes on resize, a color that changes with the theme, padding that changes at a breakpoint. It also makes the browser track far more than it needs to.

Not recommendedtransition-all
Recommendedtransition-colors
/* Say exactly what moves. */
.card {
  transition:
    translate 200ms ease-out,
    box-shadow 200ms ease-out;
}

Animate what the compositor can handle

Some properties are cheap to animate because the GPU can move pixels that were already painted: transform, translate, scale, rotate, opacity and usually filter. Others force the browser to lay out or repaint the page on every frame: width, height, top, left, margin. If you want something to grow, scale it or animate a grid-template-rows trick, rather than animating its height directly.

Fixing the one-pixel shimmy

Sometimes an element wobbles by a pixel while it moves or at the moment the animation ends, often text snapping to a different position. It happens when the browser switches the element between being painted normally and being drawn as its own GPU layer. Safari on iOS is the usual place you will see it.

Hover me
.sliding-panel {
  will-change: transform;
}

will-change asks the browser to keep the element on its own layer the whole time, so nothing changes at the start or end. Use it on the specific elements that shimmy, not as a blanket rule: each layer costs memory, and hundreds of them make things slower.

Switching themes without a color wave

When you toggle dark mode, every element with a color transition animates on its own timing. Buttons fade at 150ms, cards at 200ms, text instantly, and for a moment the page is a patchwork of both themes. Turn every transition off for the duration of the switch.

AppearanceSyncs with your system.
Save
Not recommended
AppearanceSyncs with your system.
Save
Recommended
function setTheme(theme: "light" | "dark") {
  const style = document.createElement("style");
  style.textContent = "*, *::before, *::after { transition: none !important; }";
  document.head.appendChild(style);

  document.documentElement.dataset.theme = theme;

  // Force the new colors to apply, then restore transitions next frame.
  window.getComputedStyle(document.body).color;
  requestAnimationFrame(() => style.remove());
}

If you use next-themes, the disableTransitionOnChange prop does exactly this.

Checklist

  • Toggleable states use transitions; one-shot sequences use keyframes.
  • No transition: all anywhere in the codebase.
  • Movement uses transform, scale, opacity or filter, not layout properties.
  • will-change is applied only where jitter actually shows up.
  • Theme switches disable transitions for one frame.

This piece expands on the Motion rules in my cheat sheet, where every rule has a live example.

Keep readingCSS typography for interfacesBalanced headings, numbers that do not jitter, readable line lengths and text that never escapes its box.