← all topics

Scroll-Driven

Framer Motion

A MotionValue tracks the container's scrollTop. Each card reads its own offsetTop after mount and uses useTransform to map scroll position to animation progress.

↓ scroll down
01

Fade & Rise

Opacity and Y translate tied to scroll progress.

02

Scale In

Scale from 0.85→1 as the card enters the viewport.

03

Rotate

A subtle Z rotation unwinds from ±6° to 0° on scroll.

04

Slide Left

Translates in from -40px on the x-axis.

05

Blur Clear

A blur filter resolves from 10px to 0 as it scrolls in.

06

Clip Reveal

clip-path wipes right-to-left as the card enters.

how it works

A MotionValue tracks scrollTop via a plain scroll listener — no re-renders.

Each card reads its offsetTop on mount, then useTransform maps:

scrollTop = offsetTop − 500
→ progress 0 (card just entering)

scrollTop = offsetTop − 280
→ progress 1 (animation done)

concepts

MotionValue

A reactive container that updates without triggering React re-renders. Framer Motion reads it synchronously to style elements directly each frame.

useTransform

Maps one MotionValue range to another — like a scale function for reactive values. scrollTop [start, end] → progress [0, 1] → CSS property value.

per-element scroll windows

Each card reads its own offsetTop on mount and defines a unique [start, end] scroll range. Cards higher in the list animate at different scroll positions.

useSpring wrapping

Wrapping a derived value in useSpring smooths sudden scroll jumps into organic deceleration, so fast scrolling doesn't cause jarring animation snaps.

core pattern

const scrollMV = useMotionValue(0);
container.addEventListener("scroll", () =>
  scrollMV.set(container.scrollTop)
);

// Per card — unique animation window based on its offsetTop
const progress = useTransform(
  scrollMV,
  [cardTop - containerH, cardTop - containerH + 220],
  [0, 1],
  { clamp: true }
);
const opacity = useTransform(progress, [0, 1], [0, 1]);
const y       = useTransform(progress, [0, 1], [40, 0]);