Scroll-Driven
Framer MotionA MotionValue tracks the container's scrollTop. Each card reads its own offsetTop after mount and uses useTransform to map scroll position to animation progress.
Fade & Rise
Opacity and Y translate tied to scroll progress.
Scale In
Scale from 0.85→1 as the card enters the viewport.
Rotate
A subtle Z rotation unwinds from ±6° to 0° on scroll.
Slide Left
Translates in from -40px on the x-axis.
Blur Clear
A blur filter resolves from 10px to 0 as it scrolls in.
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
A reactive container that updates without triggering React re-renders. Framer Motion reads it synchronously to style elements directly each frame.
Maps one MotionValue range to another — like a scale function for reactive values. scrollTop [start, end] → progress [0, 1] → CSS property value.
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.
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]);