Interruptible UI: CSS Transitions vs WAAPI
Interfaces are interrupted constantly. A user opens a menu and closes it before it finishes opening; hovers across a row of cards; drags a sheet halfway, lets go, then grabs it again. An animation system that only looks good when left alone looks broken in real use: elements jump, restart from the beginning, or finish an outdated motion before starting the new one. The three animation tools on the web — CSS transitions, CSS keyframe animations and the Web Animations API — handle interruption very differently. This page compares them and shows which to reach for when motion must be interruptible. It belongs to CSS Animation vs Web Animations API in the CSS-Only Micro-Interactions & Animations guide.
Three behaviours under interruption
Transitions retarget. A transition runs from the current computed value to the new target. If the target changes mid-flight, a new transition starts from wherever the element is now. The motion turns around smoothly, with a proportionally shortened duration when it reverses to the original value.
Keyframe animations restart or snap. A keyframe animation always runs from its first keyframe. If the class that applies it is removed mid-flight, the element snaps to its unanimated style immediately; if another animation is applied, that one starts from its own first keyframe, not from where the element currently is.
WAAPI does what you tell it. An element.animate() call can start from any value, including the current on-screen one read from the running animation. That flexibility is what gesture-driven and physics-based motion needs, at the cost of writing the logic yourself.
The complete implementation
Hover each track in the demo and leave it again before the box arrives. The first uses a transition and turns around smoothly. The second applies a keyframe animation on hover and snaps back when the pointer leaves.
/* Hover the track, not the moving box, so the box can move
away from the pointer without ending the hover. */
/* Interruptible: a transition retargets from the current value. */
.box--transition {
transition: translate 600ms cubic-bezier(0.2, 0, 0, 1);
}
.track:hover .box--transition,
.track:focus-within .box--transition {
translate: 8rem 0;
}
/* Not interruptible: the animation exists only while hovered. */
.track:hover .box--keyframes,
.track:focus-within .box--keyframes {
animation: slide 600ms cubic-bezier(0.2, 0, 0, 1) forwards;
}
@keyframes slide {
to { translate: 8rem 0; }
}
@media (prefers-reduced-motion: reduce) {
.box--transition { transition-duration: 1ms; }
.track:hover .box--keyframes,
.track:focus-within .box--keyframes { animation-duration: 1ms; }
}
The keyframe version has no "leave" motion at all: when :hover stops matching, the animation is removed and the element's style reverts instantly. You could add a second animation for leaving, but it would start from its own first keyframe — the fully moved position — even if the entering animation had only got a quarter of the way. That jump is the fundamental problem with using keyframes for state changes. Notice also that the demo hovers a track rather than the moving box itself. If the hover target were the box, sliding it away from the pointer would end the hover mid-move, and the transition version would flicker back and forth — an interruption loop the user never asked for.
The key technique: states with transitions, sequences with keyframes
The dividing line is simple. When motion expresses a change between two states — open and closed, hovered and not, selected and not — use a transition, because states can change at any moment and transitions handle that for free. When motion is a self-contained sequence that plays through once — an attention nudge, a loading loop, a celebratory burst — use keyframes, because sequences are rarely interrupted and keyframes express multi-step motion better.
Entry animations are a special case. Since @starting-style arrived, elements appearing in the DOM can use transitions for their entry, which makes their exit interruptible too: a popover closed while still opening reverses from its current position. Starting-Style Entry Animations covers the technique; it is usually better than a keyframe entry for anything the user can dismiss.
When the Web Animations API earns its place
Transitions retarget, but only along a fixed curve toward a new target, with no memory of speed. Three situations need more:
- Gestures with momentum. When a user flings a bottom sheet, the release animation should start at the finger's velocity. A CSS transition always starts from rest. With WAAPI you read the current position, compute an easing or a
linear()spring curve that matches the release velocity, and animate from there. - Handing off between animations. If an element is mid-way through one animation and must start a different one — not a reversal, a new path — WAAPI can read the current value with
getComputedStyle, callcommitStyles()to freeze it into the inline style, cancel the old animation, and start the new one from exactly that point. - Coordinated interruption. When interrupting one element should also re-time others — a list where removing an item mid-animation must re-flow its neighbours — script needs to see and adjust every running animation, which
getAnimations()makes possible.
// Hand off from a running animation to a new one without a jump.
function retarget(el, running, keyframesTo, options) {
running.commitStyles(); // write the current on-screen value inline
running.cancel(); // stop the old animation
return el.animate(keyframesTo, options); // starts from the committed value
}
commitStyles() requires the element to be rendered and writes the animated properties into the style attribute. After the new animation finishes, remember that the inline style is still there; either commit the final value too or clean up the inline properties.
Proportional reversal: a detail transitions get right
When a transition is interrupted and heads back to its original value, browsers shorten the new transition in proportion to how far the first had progressed. A 300ms opening transition interrupted after 60% of its progress reverses in roughly 180ms, not a full 300ms. Without this, quick hover-in, hover-out movements would feel sluggish, because every reversal would take the full duration regardless of how little distance remained. Keyframe animations and naive WAAPI code do not do this automatically; if you build interruption logic in script, scale the reverse duration by the progress yourself. Interrupted and Reversing Transitions shows the effect in detail.
Testing interruption deliberately
Interruption bugs hide in demos because demos are usually played through calmly. Make interruption part of reviewing any animation: toggle it twice quickly, hover in and out repeatedly, press a key while the previous press is still animating, and resize the window mid-transition. Slow the animation down with the DevTools Animations panel to see exactly what happens at the moment of interruption. A good result looks the same at every playback speed: the element always moves continuously from where it is, and never jumps to a position it was not at.
Browser support
CSS transitions and keyframe animations are supported in every current browser; the transition shorthand dates from Chrome 26, Edge 12, Firefox 16 and Safari 9. The Web Animations API (element.animate) is supported in Chrome 36+, Edge 79+, Firefox 48+ and Safari 13.1+; newer members such as commitStyles() arrived in later versions, so check the specific methods you use. @starting-style is supported in Chrome and Edge 117+, Firefox 129+ and Safari 17.5+, and linear() easing in Chrome and Edge 113+, Firefox 112+ and Safari 17.2+.
FAQ
Why do CSS transitions handle interruption better than keyframe animations? A transition always starts from the element's current computed value, so if the target changes mid-flight it turns around from wherever it is. A keyframe animation has fixed start and end keyframes; removing it snaps the element to its unanimated style and re-adding it restarts from the first keyframe.
When do I need the Web Animations API for interruptions? When the new animation must start from the current on-screen value and follow a path CSS cannot express, such as continuing a drag gesture with its velocity, or when several interrupted animations must be coordinated. WAAPI can read the current value and build a new animation from it.
What does commitStyles() do? It writes an animation's current computed values into the element's inline style. Calling it before cancelling an animation freezes the element where it is, so a following animation can start from that point instead of jumping.
Do interrupted transitions keep their full duration? No. When a transition reverses back toward its original value before finishing, browsers shorten the reverse duration in proportion to how far it had progressed, so a half-finished 300ms transition takes about 150ms to return.
Related
- CSS Animation vs Web Animations API — the parent guide.
- Controlling CSS Animations From JavaScript — getAnimations() and the finished promise.
- When to Use JavaScript Animation — the broader decision.
- Spring and Bounce Easing With linear() — spring curves for release animations.
Related articles
More pages in the same section.