Transitions for interruptions

Use transitions for rapid triggers: they retarget; keyframes restart.

Emil Kowalski@emilkowalski
One nice subtle benefit of using Base UI over Radix is that the animations are interruptible since Base uses CSS transitions instead of keyframes. This is me spamming on both dropdowns, notice the glitchiness in the one on the left.
Sep 17, 2026View on X ↗
Emil KowalskiBuilding a toast component
CSS transitions, on the other hand, can be interrupted and retargeted, even before the first transition has finished, so I used them instead.
emilkowal.skiRead article ↗

From emil-design-eng; demo 2 is the Sonner stack.

Spam a dropdown

Click each Options fast, or press Spam both.

@keyframes (Radix)Bad
opacity, last 3s
animation: menu-in / menu-out
transition (Base UI)Good
opacity, last 3s
transition: opacity, transform
.menu {
  opacity: 0;
  transform: scale(0.95) translateY(-4px);
  transform-origin: top;
  pointer-events: none;
  transition: opacity 200ms var(--ease-out),
              transform 200ms var(--ease-out);
}
.menu[data-state="open"] {
  opacity: 1;
  transform: none;
  pointer-events: auto;
  transition-duration: 250ms;
}

/* Not this: restarts at "from" */
.menu[data-state="open"]   { animation: menu-in 250ms forwards; }
.menu[data-state="closed"] { animation: menu-out 200ms forwards; }

Add toasts rapidly

Press Burst, or Add toast repeatedly.

@keyframes per moveBad
animation: slide-in / shift
transitionGood
transition: transform 300ms var(--ease-out)
.toast {
  transform: translateY(calc(var(--i) * -10px)) scale(calc(1 - var(--i) * 0.05));
  transition: transform 300ms var(--ease-out), opacity 300ms var(--ease-out);
}
.toast[data-mounted="false"] { transform: translateY(110%); opacity: 0; }

toaster.append(el);
el.offsetHeight;
el.dataset.mounted = "true";
others.forEach((t, i) => t.style.setProperty("--i", i + 1));

Why it matters, pros and cons

Why it matters

A transition starts from the current value; a keyframe jumps back to from.

Pros

  • Reversible at any point, no JS.
  • One state attribute; CSS derives the motion.
  • No separate exit keyframes to maintain.

Cons

  • Entrances need a committed start state.
  • Multi-step motion and loaders still need keyframes.
  • Retargeting keeps position, not velocity; gestures want springs.