# Pause Offscreen Animations

**MUST** · **ID:** `animations-ibelick-pause-offscreen` · **Category:** animations
**Source:** [@Ibelick](https://www.ui-skills.com/)
**Interactive version:** https://ui-guides-agent-rules.netlify.app/principles/animations-ibelick-pause-offscreen

> MUST: Pause looping animations when off-screen using IntersectionObserver. Saves CPU/battery on mobile. Resume when element enters viewport.

Pause looping animations when they're not visible to save battery and CPU

An off-screen loop is pure waste: it keeps the compositor awake, burns CPU and battery, and produces exactly zero pixels anyone sees. The browser does not stop it for you — a CSS animation on a scrolled-past element runs forever. ibelick's fixing-motion-performance skill names the mechanism ("use IntersectionObserver for visibility and pausing"): observe the element, and toggle `animation-play-state: paused` or stop the rAF loop when it leaves the viewport. Note the scope is LOOPING animations — a one-shot entrance that finishes off-screen costs nothing.

## Bad — do not do this

`animations-ibelick-pause-offscreen-bad`

```tsx
export function IbelickPauseOffscreenBad() {
  return (
    <div className="space-y-4">
      <div className="flex items-center gap-3 p-4 bg-muted rounded-lg">
        <div
          className="size-8 border-4 border-primary border-t-transparent rounded-full"
          style={{ animation: 'spin 1s linear infinite' }}
        />
        <span className="text-sm">Loading data...</span>
      </div>
      <style>{`
        @keyframes spin {
          to { transform: rotate(360deg); }
        }
      `}</style>
      <p className="text-xs text-destructive">
        Spinner animates forever even when scrolled off screen - wastes battery
      </p>
    </div>
  );
}
```

## Good — do this

`animations-ibelick-pause-offscreen-good`

```tsx
import { useState, useEffect, useRef } from 'react';

export function IbelickPauseOffscreenGood() {
  const [isVisible, setIsVisible] = useState(true);
  const ref = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => setIsVisible(entry.isIntersecting),
      { threshold: 0.1 }
    );

    if (ref.current) {
      observer.observe(ref.current);
    }

    return () => observer.disconnect();
  }, []);

  return (
    <div className="space-y-4" ref={ref}>
      <div className="flex items-center gap-3 p-4 bg-muted rounded-lg">
        <div
          className="size-8 border-4 border-primary border-t-transparent rounded-full"
          style={{
            animation: 'spin 1s linear infinite',
            animationPlayState: isVisible ? 'running' : 'paused',
          }}
        />
        <span className="text-sm">Loading data... {isVisible ? '(visible)' : '(paused)'}</span>
      </div>
      <style>{`
        @keyframes spin {
          to { transform: rotate(360deg); }
        }
      `}</style>
      <p className="text-xs text-success">
        IntersectionObserver pauses animation when off screen
      </p>
    </div>
  );
}
```

## References

- [ibelick — baseline-ui SKILL.md](https://github.com/ibelick/ui-skills/blob/main/skills/baseline-ui/SKILL.md)
- [ibelick — fixing-motion-performance SKILL.md](https://github.com/ibelick/ui-skills/blob/main/skills/fixing-motion-performance/SKILL.md)
- [IntersectionObserver API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API)
