Scroll-driven animations create that cinematic feel where content responds to the user's scroll position. With Framer Motion's built-in scroll hooks, you don't need additional libraries like GSAP ScrollTrigger or Locomotive Scroll.
The Core Hooks
Framer Motion gives you three hooks that work together:
useScroll
Returns a MotionValue representing scroll progress (0 to 1) for a target element or the page.
import { useScroll } from "framer-motion";
// Page-level scroll
const { scrollYProgress } = useScroll();
// Element-level scroll (when element enters/exits viewport)
const ref = useRef(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ["start end", "end start"],
});The offset prop controls when tracking starts and ends:
"start end"— tracking begins when the element's top reaches the viewport bottom"end start"— tracking ends when the element's bottom passes the viewport top
useTransform
Maps one value range to another. This is how you convert scroll progress into animation values.
import { useTransform } from "framer-motion";
const opacity = useTransform(scrollYProgress, [0, 0.5, 1], [0, 1, 0]);
const scale = useTransform(scrollYProgress, [0, 1], [0.8, 1.2]);
const y = useTransform(scrollYProgress, [0, 1], [100, -100]);useSpring
Wraps any MotionValue with spring physics for smoother transitions.
import { useSpring } from "framer-motion";
const smoothProgress = useSpring(scrollYProgress, {
stiffness: 100,
damping: 30,
restDelta: 0.001,
});Pattern 1: Fade + Slide on Enter
The most common scroll animation. Content fades in and slides up as it enters the viewport.
function FadeInSection({ children }) {
const ref = useRef(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ["start end", "end start"],
});
const opacity = useTransform(scrollYProgress, [0, 0.3], [0, 1]);
const y = useTransform(scrollYProgress, [0, 0.3], [60, 0]);
return (
<motion.div ref={ref} style={{ opacity, y }}>
{children}
</motion.div>
);
}Pattern 2: Parallax Layers
Different elements scroll at different speeds, creating depth.
function ParallaxSection() {
const ref = useRef(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ["start end", "end start"],
});
const bgY = useTransform(scrollYProgress, [0, 1], ["-20%", "20%"]);
const fgY = useTransform(scrollYProgress, [0, 1], ["10%", "-10%"]);
return (
<div ref={ref} className="relative overflow-hidden">
<motion.div style={{ y: bgY }} className="absolute inset-0">
{/* Background layer */}
</motion.div>
<motion.div style={{ y: fgY }} className="relative z-10">
{/* Foreground content */}
</motion.div>
</div>
);
}Pattern 3: Progress-Based Color Fill
Words or elements change color based on scroll position — the karaoke effect.
function ScrollHighlight({ words }) {
const ref = useRef(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ["start 0.8", "end 0.2"],
});
return (
<p ref={ref} className="flex flex-wrap gap-x-2">
{words.map((word, i) => {
const start = i / words.length;
const end = (i + 1) / words.length;
const color = useTransform(
scrollYProgress,
[start, end],
["rgba(0,0,0,0.2)", "rgba(0,0,0,1)"]
);
return (
<motion.span key={i} style={{ color }}>
{word}
</motion.span>
);
})}
</p>
);
}See it live: Text Highlight on Scroll
Pattern 4: Sticky Scroll with Content Swap
A container stays pinned while content inside changes based on scroll position.
function StickyScroll({ sections }) {
const ref = useRef(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ["start start", "end end"],
});
return (
<div ref={ref} style={{ height: `${sections.length * 100}vh` }}>
<div className="sticky top-0 h-screen flex items-center">
{sections.map((section, i) => {
const start = i / sections.length;
const end = (i + 1) / sections.length;
const opacity = useTransform(
scrollYProgress,
[start, start + 0.1, end - 0.1, end],
[0, 1, 1, 0]
);
return (
<motion.div
key={i}
style={{ opacity }}
className="absolute inset-0"
>
{section}
</motion.div>
);
})}
</div>
</div>
);
}Performance Tips
-
Use
MotionValuedirectly — Pass values viastyle={{ opacity }}instead of state. Motion values bypass React re-renders. -
Avoid
useTransformin render loops — Create transforms once at the component level, not inside.map()callbacks that re-run on every render. -
Use
useSpringsparingly — Spring physics on scroll values add smoothness but also latency. Only apply to values where the lag feels intentional. -
Set
layoutScroll— If your scroll container isn't the viewport, uselayoutScrollon the container for accurate measurements. -
Prefer
will-change: transform— Framer Motion adds this automatically, but verify in DevTools that animated elements are on their own compositing layer.
Related Components
- Text Highlight on Scroll — Karaoke-style word highlighting
- Before/After Slider — Drag-based content comparison
- Circular Progress Ring — Animated SVG with scroll trigger potential
Browse all animation components on Froiden UI.