How to Implement Interruptible Animations with CSS Transitions vs Keyframes: A Complete Guide
CSS transitions provide built-in interruptibility by retargeting from the current visual state when a property changes, while keyframe animations restart from the beginning and require JavaScript workarounds to achieve similar behavior.
Interruptible animations are essential for fluid, responsive UIs where users may trigger new states before previous animations finish—think rapidly adding toasts, toggling menus, or dragging elements. According to the emilkowalski/skills repository's design engineering standards, understanding when and how to use CSS transitions versus keyframes for interruptible animations separates polished interfaces from jarring, jumpy experiences.
Why CSS Transitions Excel at Interruptibility
The skills/emil-design-eng/SKILL.md file at line 269 establishes a clear principle: use CSS transitions over keyframes for interruptible UI. Three core mechanisms make this possible:
- Retargetable animations — when a property changes mid-transition, the browser seamlessly continues from the element's current computed value instead of snapping back to the start
- Hardware acceleration — transitions on
transformandopacityrun on the compositor thread, maintaining smooth motion even under main thread load (line 511) - Zero JavaScript overhead — declarative CSS reduces bundle size and automatically respects
prefers-reduced-motion
This retargeting behavior is the critical difference. With keyframes, interrupting an animation typically causes a visual "jump-back-to-zero" that feels broken. With transitions, the browser calculates a new animation curve from wherever the element currently sits.
When to Use Transitions vs. Alternatives
The skills/review-animations/STANDARDS.md and related files provide a decision framework:
| Situation | Recommended Approach |
|---|---|
| Rapidly-triggered UI (toasts, toggles, dropdowns) | CSS transition on transform/opacity |
| Gesture-driven motion (drag, swipe, bottom sheet) | Spring-based library (Framer Motion) for velocity-aware interruptibility |
| Long decorative sequences (onboarding demos) | CSS @keyframes acceptable—interruptibility less critical |
| Dynamic programmatic control (progress bars, timelines) | Web Animations API (WAAPI) for precise timing with CSS performance |
The skills/improve-animations/SKILL.md severity matrix flags non-interruptible dynamic UI as a high-severity issue, making this choice architecturally significant.
Implementation: Interruptible Toast with CSS Transitions
This pattern from the repository demonstrates core interruptibility:
/* toast.css */
.toast {
opacity: 0;
transform: translateY(100%);
transition: opacity 400ms ease, transform 400ms ease;
}
.toast[data-visible] {
opacity: 1;
transform: translateY(0);
}
const toast = document.getElementById('toast');
function show() {
toast.setAttribute('data-visible', '');
}
function hide() {
toast.removeAttribute('data-visible');
}
// Simulate rapid toggling—transition retargets from current state
show();
setTimeout(hide, 150); // hides mid-animation
setTimeout(show, 250); // re-shows smoothly from partial position
Why this works: When data-visible toggles while the transition is in-flight, the browser recalculates end values and continues from the current visual state. No jump. No restart. The skills/review-animations/SKILL.md checklist explicitly validates this behavior during code review.
Modern Approach: @starting-style for Enter Animations
The repository highlights @starting-style (future-proof CSS) at line 269-274 as a cleaner alternative to JS-driven mount animations:
.modern-toast {
opacity: 1;
transform: translateY(0);
transition: opacity 400ms ease, transform 400ms ease;
@starting-style {
opacity: 0;
transform: translateY(100%);
}
}
This eliminates the common pattern of mounting-then-animating via JavaScript. The browser treats the @starting-style block as the initial state and automatically animates to the final state on element insertion.
Programmatic Control: Web Animations API (WAAPI)
When you need JavaScript control without sacrificing interruptibility, skills/emil-design-eng/SKILL.md at line 515 recommends WAAPI:
let animation;
function showToast() {
if (animation) animation.cancel();
animation = toast.animate(
[
{ opacity: 0, transform: 'translateY(100%)' },
{ opacity: 1, transform: 'translateY(0)' }
],
{
duration: 400,
easing: 'cubic-bezier(0.23, 1, 0.32, 1)',
fill: 'forwards'
}
);
}
function hideToast() {
if (animation) animation.cancel();
animation = toast.animate(
[
{ opacity: 1, transform: 'translateY(0)'' },
{ opacity: 0, transform: 'translateY(100%)' }
],
{
duration: 300,
easing: 'cubic-bezier(0.23, 1, 0.32, 1)',
fill: 'forwards'
}
);
}
Key implementation detail: always cancel() the existing animation before creating a new one. This prevents conflicts and maintains interruptibility. WAAPI delivers hardware-accelerated performance equivalent to CSS transitions while enabling dynamic duration calculations, timeline scrubbing, and reverse playback.
Critical Implementation Checklist
Derived from skills/review-animations/SKILL.md:
- Avoid
transition: all— target specific properties (transform,opacity) for predictable performance - Define custom easing curves —
cubic-beziervalues create more deliberate, "punchy" motion than defaults - Leverage
@starting-style— reduce JavaScript ceremony for enter animations - Respect
prefers-reduced-motion— disable or reduce transitions for accessibility
@media (prefers-reduced-motion: reduce) {
.toast {
transition: none;
opacity: 1;
transform: none;
}
}
- Test interruptibility — rapidly toggle states in browser DevTools and confirm smooth continuation
When Keyframes Are Acceptable
CSS @keyframes remain valid for:
- One-shot decorative animations (confetti, celebration effects)
- Continuous ambient motion (loading spinners, pulsing indicators)
- Complex multi-stage sequences where precise keyframe timing matters more than interruption handling
For these cases, the skills repository suggests ensuring interruptibility via JavaScript state management or simply accepting non-interruptibility when the UX context allows.
Summary
- CSS transitions provide native interruptibility through retargeting—animations continue from current visual state rather than restarting
- Always prefer transitions for dynamic UI (toasts, menus, toggles) per
skills/emil-design-eng/SKILL.mdline 269 - Use
@starting-styleto eliminate JavaScript mount-animation boilerplate - Adopt WAAPI when programmatic control is required while maintaining CSS-level performance (line 515)
- Avoid
transition: alland always respectprefers-reduced-motion
Frequently Asked Questions
Why do my CSS keyframe animations jump when interrupted?
CSS @keyframes animations run on a fixed timeline from 0% to 100%. When interrupted and restarted, they begin from the 0% keyframe regardless of current visual state. This creates the "jump-back" effect. CSS transitions solve this by retargeting—calculating a new animation curve from wherever the element currently sits. For interruptible UI, use transitions on transform and opacity instead.
Can I make keyframe animations interruptible with JavaScript?
Yes, but it requires significant complexity. You must track animation progress via getComputedStyle, calculate the current keyframe percentage, then construct a new animation starting from that state. The skills/review-animations/STANDARDS.md recommends avoiding this approach—use transitions for interruptible UI or spring-based libraries for gesture-driven motion where velocity preservation matters.
How do I test if my animation is truly interruptible?
Rapidly toggle the triggering state in your browser. For a toast, click show/hide repeatedly with ~150ms intervals. A properly interruptible transition will smoothly reverse direction from its current position. A non-interruptible animation will snap to start points or exhibit visual glitches. The skills/review-animations/SKILL.md explicitly lists this as a required review step.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →