# CSS Transitions vs Keyframes for Interruptible UI Animations: When to Use Each

> Choose between CSS transitions and @keyframes for UI animations. Learn when to use each for smooth, interruptible animations versus restarting ones.

- Repository: [Emil Kowalski/skills](https://github.com/emilkowalski/skills)
- Tags: deep-dive
- Published: 2026-08-02

---

**CSS transitions are retargetable mid-animation, making them ideal for interruptible UI; @keyframes always restart from frame 0, causing jarring jumps when interrupted.**

Choosing between CSS transitions and `@keyframes` animations directly impacts how fluidly your interface responds to rapid user input. This guide draws from the animation standards defined in the **emilkowalski/skills** repository to explain why interruptibility matters and how to implement it correctly.

---

## What Makes an Animation Interruptible?

An **interruptible animation** can respond to new input without visual discontinuity. When a user toggles a toast, closes a modal, or drags a panel, the motion should adapt smoothly rather than snapping or restarting.

According to [`skills/review-animations/STANDARDS.md`](https://github.com/emilkowalski/skills/blob/main/skills/review-animations/STANDARDS.md):

> "CSS **transitions** can be interrupted and retargeted mid‑animation; **keyframes** restart from zero. For anything triggered rapidly (toasts being added, toggles), transitions are smoother."

This distinction forms the foundation of the repository's motion philosophy.

---

## CSS Transitions: The Retargetable Choice

**CSS transitions excel at responsive, state-based motion.** When a property's target value changes during an active transition, the browser calculates a new path from the **current intermediate state** to the updated target.

### Key Characteristics

| Aspect | Behavior |
|--------|----------|
| **Retargetability** | ✅ Smooth continuation from current state |
| **Implementation** | Single `transition` declaration |
| **Performance** | GPU-accelerated for `transform`/`opacity` |
| **Best for** | Toggles, toasts, hover states, collapsible panels |

### Practical Example: Interruptible Toast

```html
<div class="toast">New message</div>

<style>
.toast {
  opacity: 0;
  transform: translateY(100%);
  transition: opacity 400ms ease, transform 400ms ease;
}
.toast[data-visible] {
  opacity: 1;
  transform: translateY(0);
}
</style>

<script>
function showToast() {
  const el = document.querySelector('.toast');
  el.dataset.visible = "";
  // Rapid subsequent calls retarget the transition smoothly
}
</script>

```

Calling `showToast()` repeatedly before the previous animation completes does not restart from hidden—the toast simply adjusts its trajectory toward visible. This pattern appears throughout [`skills/find-animation-opportunities/SKILL.md`](https://github.com/emilkowalski/skills/blob/main/skills/find-animation-opportunities/SKILL.md) for accordion panels, dropdown menus, and notification stacks.

---

## CSS Keyframes: The Sequential Choice

**`@keyframes` animations always execute their defined sequence from the beginning.** When restarted or when play state changes, the animation jumps to frame 0, regardless of current progress.

### Interruptibility Problem

```css
@keyframes logoFadeIn {
  from { opacity: 0; transform: scale(0.9); }
  to   { opacity: 1; transform: scale(1); }
}
.logo {
  animation: logoFadeIn 800ms ease-out forwards;
}

```

If this animation is interrupted and retriggered, the element **snaps back to 90% scale and zero opacity** before fading in again. For rapid interactions, this creates visual discontinuity.

### When Keyframes Are Appropriate

- **One-off entrance sequences** (splash screens, brand reveals)
- **Decorative loops** (loading spinners, background ambient motion)
- **Fixed timing choreography** where interruption is never expected

As noted in [`skills/improve-animations/AUDIT.md`](https://github.com/emilkowalski/skills/blob/main/skills/improve-animations/AUDIT.md), keyframes should be replaced with transitions for any UI element that responds to user actions.

---

## Real-Time Gestures: Beyond Both Options

When animation must follow **continuous user input** (drag, swipe, momentum), neither CSS transitions nor keyframes provide sufficient control. The repository recommends **spring-based motion** via the Web Animations API (WAAPI) or JavaScript libraries.

### WAAPI Example: Gesture-Driven Panel

```js
const panel = document.querySelector('.sheet');
let anim = panel.animate(
  [{ transform: 'translateX(0)' }, { transform: 'translateX(-100%)' }],
  { duration: 500, easing: 'cubic-bezier(0.23,1,0.32,1)', fill: 'forwards' }
);

// Update target on the fly—the animation retargets instantly
function updateTarget(percent) {
  anim.effect.updateTiming({ duration: 500 * (1 - percent) });
}

```

This approach, documented in [`STANDARDS.md`](https://github.com/emilkowalski/skills/blob/main/STANDARDS.md), preserves interruptibility while enabling physics-based response to gesture velocity.

---

## Decision Framework from the Repository

The **Emil Design Engineering** skill file provides explicit guidance in [`skills/emil-design-eng/SKILL.md`](https://github.com/emilkowalski/skills/blob/main/skills/emil-design-eng/SKILL.md):

> "Use CSS transitions over keyframes for interruptible UI … CSS transitions can be interrupted and retargeted mid‑animation; keyframes restart from zero."

Apply this hierarchy:

1. **Default to transitions** for any property that changes based on state or user action
2. **Reserve keyframes** for static, non-interruptible sequences
3. **Implement springs** when real-time input drives the motion

---

## Performance Considerations

Both transitions and keyframes achieve **GPU acceleration** when limited to compositor-friendly properties:

- `transform` (translate, scale, rotate)
- `opacity`

However, keyframes carry hidden costs when interrupted. The forced restart can trigger:

- **Layout recalculation** if position-dependent properties animate
- **Paint operations** for properties affecting visual appearance
- **Frame drops** from main-thread work during the jump

Transitions avoid this by maintaining continuous computation on the compositor thread.

---

## Summary

- **CSS transitions** retarget smoothly from current state when values change—essential for any UI that toggles rapidly
- **`@keyframes` animations** restart from frame 0, creating jarring discontinuities when interrupted
- **Reserve keyframes** strictly for one-off, non-interactive sequences like brand reveals
- **Use springs** (WAAPI or JS) for gesture-driven motion requiring real-time response
- **Animate only `transform` and `opacity`** to maintain 60fps performance on both techniques

---

## Frequently Asked Questions

### Why do my keyframe animations feel "janky" on repeated clicks?

`@keyframes` animations always restart from the first keyframe when retriggered. If a user clicks rapidly, each restart forces the element to jump back to its initial state before animating forward. Replace with CSS transitions to enable smooth retargeting from the current position.

### Can I interrupt a CSS transition and reverse it immediately?

Yes. Changing the target class or property value mid-transition causes the browser to calculate a new path from the **current computed value** to the new target. This produces natural, spring-like motion without JavaScript.

### When is Web Animations API preferred over CSS transitions?

WAAPI becomes necessary when animation must respond to **continuous input** like drag gestures or momentum scrolling. CSS transitions handle discrete state changes; WAAPI provides programmatic control for real-time physics simulation.

### Are there accessibility concerns with interruptible animations?

Rapid motion can trigger vestibular disorders. The repository's standards in [`STANDARDS.md`](https://github.com/emilkowalski/skills/blob/main/STANDARDS.md) recommend respecting `prefers-reduced-motion` media queries, which should disable or simplify both transitions and keyframes regardless of interruptibility.