# Interruptible

**MUST** · **ID:** `animations-interruptible` · **Category:** animations
**Source:** [Vercel](https://github.com/vercel-labs/agent-skills/blob/main/skills/web-design-guidelines/SKILL.md)
**Interactive version:** https://ui-guides-agent-rules.netlify.app/principles/animations-interruptible

> MUST: Animations are interruptible and input-driven (avoid autoplay)

Animations are cancelable by user input

Don't force users to wait for animations to complete. If a user interacts during an animation (clicking, scrolling, etc.), the animation should either instantly complete or transition to the new state. Never block interaction during animation.

## Bad — do not do this

`animations-interruptible-bad`

```tsx
import { useState } from 'react';

export function InterruptibleBad() {
  const [isAnimating, setIsAnimating] = useState(false);
  const [position, setPosition] = useState(0);

  const startAnimation = () => {
    if (isAnimating) return;

    setIsAnimating(true);
    setPosition(0);

    const duration = 2000;
    const startTime = Date.now();

    const animate = () => {
      const elapsed = Date.now() - startTime;
      const progress = Math.min(elapsed / duration, 1);
      setPosition(progress * 300);

      if (progress < 1) {
        requestAnimationFrame(animate);
      } else {
        setIsAnimating(false);
      }
    };

    animate();
  };

  return (
    <div className="w-full max-w-sm">
      <button
        onClick={startAnimation}
        disabled={isAnimating}
        className="px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors disabled:bg-muted-foreground disabled:cursor-not-allowed focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
      >
        Start Animation
      </button>
      <div className="mt-4 h-16 bg-muted rounded-lg relative overflow-hidden">
        <div
          className="absolute top-4 w-12 h-8 bg-primary rounded"
          style={{ left: `${position}px` }}
        />
      </div>
      <p className="text-xs text-error mt-4">
        Animation can't be interrupted. Button disabled during animation.
      </p>
    </div>
  );
}
```

## Good — do this

`animations-interruptible-good`

```tsx
import { useState } from 'react';

export function InterruptibleGood() {
  const [position, setPosition] = useState<'start' | 'end'>('start');
  const [isMoving, setIsMoving] = useState(false);

  const moveBox = () => {
    setIsMoving(true);
    setPosition(position === 'start' ? 'end' : 'start');
    setTimeout(() => setIsMoving(false), 500);
  };

  return (
    <div className="w-full max-w-sm">
      <button
        onClick={moveBox}
        className="px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
      >
        {isMoving ? 'Moving…' : 'Move Box'}
      </button>
      <div className="mt-4 h-16 bg-muted rounded-lg relative overflow-hidden">
        <div
          className={`absolute top-4 w-12 h-8 bg-primary rounded transition-transform duration-500 ease-in-out ${
            position === 'end' ? 'translate-x-[300px]' : 'translate-x-0'
          }`}
        />
      </div>
      <p className="text-xs text-success mt-4">
        Clicking again interrupts and reverses the animation
      </p>
    </div>
  );
}
```

## References

- [Animation Best Practices](https://www.nngroup.com/articles/animation-usability/)
