Skip to content
Back to skills

Scroll Animation Patterns

ASecurity

Reusable scroll-linked animation patterns for React/Next.js. Covers RAF-driven character-chaos effects, sticky card decks, scroll-snap overlays, and avoids common state/ref conflicts. Use when implementing scroll-linked effects, hero reveal animations, or text/character staggered animations. Triggers on "scroll animation", "sticky card", "parallax", "character chaos", "scroll-linked", "RAF animation", "hero animation".

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 27, 2026
developmentgoreactnextjsdebuggingperformance

Security analysis

A100/100

Scanned September 27, 2026

npx -y skills add emanuelrechsteiner/claude-rcode --skill scroll-animation-patterns --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Scroll Animation Patterns?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Scroll Animation Patterns
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/emanuelrechsteiner-scroll-animation-patterns/badge)](https://www.skillsdirectory.com/skills/emanuelrechsteiner-scroll-animation-patterns)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: scroll-animation-patterns
description: Reusable scroll-linked animation patterns for React/Next.js. Covers RAF-driven character-chaos effects, sticky card decks, scroll-snap overlays, and avoids common state/ref conflicts. Use when implementing scroll-linked effects, hero reveal animations, or text/character staggered animations. Triggers on "scroll animation", "sticky card", "parallax", "character chaos", "scroll-linked", "RAF animation", "hero animation".
context: fork
model: sonnet
allowed-tools: Read, Write, Edit, Glob, Grep
---

# Scroll Animation Patterns

## Purpose

Provide battle-tested scroll-linked animation primitives extracted from a production landing page (reference implementation). Eliminates re-invention of RAF loops, ref synchronization, and CSS containment for each new project.

## When to Use

- Building a scroll-linked hero animation (text reveal, character chaos, character split)
- Implementing sticky card decks (overlapping cards revealed on scroll)
- Creating parallax or RAF-driven scroll effects without libraries (Framer Motion often overkill for simple cases)
- Debugging jank / frame drops on scroll animations

## Core Patterns

### Pattern 1 — CharacterChaos (per-character RAF-driven)

**When:** Text reveal where each character independently animates (position, rotation, opacity) based on scroll position.

**Key insights from the reference implementation:**
- Store character refs in an array; iterate in a single RAF callback
- Use `transform: translate3d()` + `opacity` only — do NOT animate `top/left/width/height`
- Add `will-change: transform, opacity` to the characters, remove after animation completes
- The single RAF loop must read scroll once, compute all character states, write transforms — never read/write interleaved (forced reflow kills frames)

```tsx
// Skeleton
function CharacterChaos({ text }: { text: string }) {
  const charsRef = useRef<(HTMLSpanElement | null)[]>([]);
  const rafRef = useRef<number>();
  const scrollYRef = useRef(0);

  useEffect(() => {
    const onScroll = () => {
      scrollYRef.current = window.scrollY;
      if (rafRef.current) return;           // RAF throttling
      rafRef.current = requestAnimationFrame(() => {
        const y = scrollYRef.current;
        charsRef.current.forEach((el, i) => {
          if (!el) return;
          const progress = computeProgress(y, i);
          el.style.transform = `translate3d(${progress * 50}px, ${progress * -20}px, 0) rotate(${progress * 15}deg)`;
          el.style.opacity = String(Math.min(progress * 2, 1));
        });
        rafRef.current = undefined;
      });
    };
    window.addEventListener("scroll", onScroll, { passive: true });
    onScroll();
    return () => {
      window.removeEventListener("scroll", onScroll);
      if (rafRef.current) cancelAnimationFrame(rafRef.current);
    };
  }, []);

  return (
    <span>
      {Array.from(text).map((ch, i) => (
        <span
          key={i}
          ref={(el) => (charsRef.current[i] = el)}
          style={{ display: "inline-block", willChange: "transform, opacity" }}
        >
          {ch === " " ? "\u00A0" : ch}
        </span>
      ))}
    </span>
  );
}
```

### Pattern 2 — Sticky Card Deck

**When:** Multiple cards stack and reveal as user scrolls (common in storytelling landing pages).

**Technique:**
- Parent container gets `position: relative; height: N * 100vh`
- Each card `position: sticky; top: 0; height: 100vh`
- Use `IntersectionObserver` (not scroll listener) to toggle per-card state
- For overlap effects, apply `transform: scale()` + `opacity` based on next-card visibility

**Gotcha:** `position: sticky` requires NO `overflow: hidden` on any ancestor. Check early.

### Pattern 3 — Scroll-Snap with Overlay

**When:** Section-by-section scroll with overlay header/footer.

```css
html { scroll-snap-type: y mandatory; }
section { scroll-snap-align: start; min-height: 100vh; }
.overlay { position: fixed; inset: 0; pointer-events: none; }
.overlay > * { pointer-events: auto; }
```

## Anti-Patterns (From React Perf Experience)

### ❌ Animating layout properties
Never animate `width`, `height`, `top`, `left`, `margin`. Always `transform` + `opacity`. Browsers composite these on GPU; layout props force reflow per frame.

### ❌ State for every scroll tick
```tsx
// ❌ WRONG — triggers re-render per scroll event
const [scrollY, setScrollY] = useState(0);
useEffect(() => {
  window.addEventListener("scroll", () => setScrollY(window.scrollY));
}, []);
```
```tsx
// ✓ RIGHT — ref + RAF, no re-renders
const scrollRef = useRef(0);
```

### ❌ Reading scroll + writing in interleaved fashion
```tsx
// ❌ WRONG — forced reflow per character
chars.forEach((el, i) => {
  const y = window.scrollY;         // read
  el.style.transform = ...;         // write
});
```
```tsx
// ✓ RIGHT — read once, write all
const y = window.scrollY;
chars.forEach((el, i) => { el.style.transform = ...; });
```

### ❌ Forgetting passive listener
`{ passive: true }` on scroll listeners allows the browser to continue scroll while JS runs. Without it, scroll stutters on heavy handlers.

### ❌ Leaving `will-change` permanently
`will-change: transform` is a hint that costs GPU memory. Add it on animation start, remove on completion. Don't set it on hundreds of elements by default.

## Performance Budget

For smooth 60fps on mid-tier mobile:
- Scroll handler total < 8ms per frame (16.6ms budget; ≤8ms leaves room for compositing)
- No layout reads between writes within a single frame
- Max ~200 animated elements with transform updates per frame
- Test on Chrome DevTools Performance panel under CPU 4x slowdown

## Integration with react-perf-check

If scroll animation feels janky:
1. Run the `react-perf-check` skill
2. Check for state/ref conflicts (ref pattern above)
3. Check for unnecessary re-renders via React DevTools Profiler
4. Check for forced reflow in Chrome DevTools Performance panel

## References

- Production landing page (reference implementation, 2026-04, author's private project)
- CharacterChaos engine: lives in the reference implementation's hero section
- Sticky Card Deck: phase-transition sections of the same reference page
- MDN: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_scroll_snap
- React scroll performance: https://web.dev/articles/rendering-performance

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…