Transitions for interruptions
Use transitions for rapid triggers: they retarget; keyframes restart.
Emil Kowalski@emilkowalskiOne 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.
Emil KowalskiBuilding a toast componentCSS transitions, on the other hand, can be interrupted and retargeted, even before the first transition has finished, so I used them instead.
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-outtransition (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 / shifttransitionGood
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.