Introduction
In the modern landscape of web development, user experience (UX) is paramount. Static pages are no longer enough to capture and retain user attention. Dynamic, interactive, and visually engaging interfaces are the new standard. One of the most effective ways to elevate a website’s UX is through animations, particularly those that respond to user interaction. Scroll-based animations, which trigger and progress as a user scrolls through a page, can transform a simple content-driven site into an immersive narrative experience. They guide the user’s focus, reveal information progressively, and add a layer of polish that signals a high-quality application.
For developers in the React ecosystem, the Framer Motion library has emerged as a go-to solution for creating sophisticated animations with a surprisingly simple and declarative API. It abstracts away the complexities of animation logic, allowing developers to describe *what* they want to animate, rather than getting bogged down in the *how*. This article provides a comprehensive guide to mastering scroll-based animations using Framer Motion. We will explore core concepts, walk through practical implementations of effects like scaling and rotating elements on scroll, dive into advanced techniques, and discuss best practices for performance and maintainability. Whether you’re building a portfolio with Next.js News or a marketing site with Gatsby News, these techniques will help you create memorable user experiences.
Section 1: Core Concepts for Scroll-Triggered Animations
Before we can make elements dance on the screen, it’s essential to understand the fundamental building blocks Framer Motion provides for handling scroll events. The library offers a set of powerful hooks that work in concert to track scroll position and map it to animation values. These hooks form the foundation of any scroll-based animation you’ll create.
The `motion` Component
The entry point to any Framer Motion animation is the motion
component. To make any HTML or SVG element animatable, you simply prepend motion.
to it. For example, a standard <div>
becomes <motion.div>
. This special component can accept animation-specific props like animate
, initial
, variants
, and, most importantly for our purposes, a style
prop that can accept special values called MotionValue
s.
The `useScroll` Hook
The useScroll
hook is the heart of scroll-triggered animations. When called, it returns a set of MotionValue
s that track the scroll position of the viewport or a specific scrollable element. The most commonly used value is scrollYProgress
, which provides a number between 0 (top of the scroll area) and 1 (bottom of the scroll area). This normalized value is incredibly useful because it’s independent of the element’s or viewport’s actual height, making your animation logic reusable and predictable.
The `useTransform` Hook
While scrollYProgress
gives us the “when,” the useTransform
hook helps us define the “what.” This hook is designed to transform the output of one MotionValue
into a new one. It takes an input MotionValue
(like scrollYProgress
), an input range (e.g., [0, 1]
), and an output range (e.g., [0, 360]
for rotation or ['0%', '100%']
for width). It then creates a new MotionValue
that automatically updates as the input changes. This allows you to declaratively map scroll progress to any animatable CSS property.
Here’s a basic example demonstrating how to set up the useScroll
hook to monitor the page’s scroll progress and log its value.
import { useEffect } from 'react';
import { useScroll, motion } from 'framer-motion';
export default function ScrollLogger() {
const { scrollYProgress } = useScroll();
useEffect(() => {
// The subscribe method returns an unsubscribe function
const unsubscribe = scrollYProgress.on("change", latest => {
console.log("Scroll Y Progress:", latest);
});
// Cleanup the subscription when the component unmounts
return () => unsubscribe();
}, [scrollYProgress]);
// We can also use a motion component to visualize the progress
return (
<motion.div
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
height: '10px',
background: 'blue',
transformOrigin: '0%',
// Link the scaleX directly to the MotionValue
scaleX: scrollYProgress,
}}
/>
);
}
In this snippet, we use useScroll
to get scrollYProgress
. We then use a useEffect
hook to subscribe to its changes and log them. More powerfully, we directly pass the scrollYProgress
MotionValue
to the scaleX
property of a motion.div
, creating a simple progress bar at the top of the page. This demonstrates the reactive nature of Framer Motion’s hooks.
Section 2: Implementing Practical Scroll Animations

With the core concepts understood, let’s build some common scroll-based animations. We will combine useScroll
and useTransform
to create effects where elements scale and rotate as the user scrolls down the page. These examples are perfect for hero sections, feature showcases, or any part of an application where you want to add a touch of dynamic flair.
Creating a Scaling Animation on Scroll
A popular effect is to have an element scale up or down as it enters or moves through the viewport. This can draw attention to key content or create a sense of depth. To achieve this, we will map the scrollYProgress
(from 0 to 1) to a desired scale range (e.g., from 0.5 to 1.2).
Let’s build a component that contains a box that grows as the user scrolls.
import { motion, useScroll, useTransform } from 'framer-motion';
import { useRef } from 'react';
// Assume some CSS to center the content and provide scrollable space
// .container { height: 200vh; display: flex; align-items: center; justify-content: center; }
// .box { width: 150px; height: 150px; background: white; border-radius: 20px; }
export default function ScalingBox() {
const containerRef = useRef(null);
// Track scroll progress within the container element
const { scrollYProgress } = useScroll({
target: containerRef,
offset: ["start end", "end start"] // Start when top of target hits bottom of viewport, end when bottom of target hits top of viewport
});
// Map scroll progress to scale
// When scrollYProgress is 0 (start), scale is 0.5
// When scrollYProgress is 0.5 (middle), scale is 1
// When scrollYProgress is 1 (end), scale is 0.5
const scale = useTransform(scrollYProgress, [0, 0.5, 1], [0.5, 1, 0.5]);
return (
<div ref={containerRef} className="container">
<motion.div
className="box"
style={{
scale // Apply the transformed MotionValue
}}
/>
</div>
);
}
In this example, we introduce a ref
attached to a container div. We pass this target
ref to useScroll
, which scopes the scroll tracking to that specific element. The offset
array provides fine-grained control over when the animation starts and ends relative to the viewport. We then use useTransform
to create a parabolic scaling effect: the box starts small, grows to its full size in the middle of the container, and then shrinks again as it scrolls out.
Creating a Rotating Animation on Scroll
Similarly, we can make an element rotate. This effect can be used for decorative elements or to signify a change in state or section. The process is nearly identical to the scaling example, but we map the scroll progress to a degree value for the rotate
property.
import { motion, useScroll, useTransform } from 'framer-motion';
import { useRef } from 'react';
// Assume similar CSS as the previous example
// .container { height: 200vh; ... }
// .icon { font-size: 100px; }
export default function RotatingIcon() {
const containerRef = useRef(null);
const { scrollYProgress } = useScroll({
target: containerRef,
offset: ["start end", "end end"] // Start when top of target hits bottom of viewport, end when bottom of target hits bottom of viewport
});
// Map scroll progress (0 to 1) to a rotation value (0deg to 360deg)
const rotate = useTransform(scrollYProgress, [0, 1], [0, 360]);
return (
<div ref={containerRef} className="container">
<motion.div
className="icon"
style={{
rotate // Apply the transformed MotionValue
}}
>
⚙️
</motion.div>
</div>
);
}
Here, the rotate
MotionValue
is created by mapping the linear progression of scrollYProgress
from 0 to 1 directly to a full 360-degree rotation. As the user scrolls through the containerRef
element, the gear icon will complete one full turn. This declarative approach, championed by libraries like Framer Motion News, makes complex, scroll-linked animations remarkably straightforward to implement in any React or React Native News project.
Section 3: Advanced Techniques and Real-World Applications
Once you’ve mastered the basics, you can combine hooks and properties to create much more complex and nuanced animations. These advanced techniques can help you build truly unique and polished user interfaces that stand out.
Adding Physics with `useSpring`
Linear animations can sometimes feel robotic. To add a more natural, organic feel, you can wrap your transformed MotionValue
with the useSpring
hook. This applies a physics-based spring simulation to the value, causing it to “catch up” to the scroll position with properties like stiffness and damping. The result is a smoother, more fluid motion that feels less directly tethered to the scrollbar.
import { motion, useScroll, useTransform, useSpring } from 'framer-motion';
import { useRef } from 'react';
export default function SmoothProgressBar() {
const { scrollYProgress } = useScroll();
// Add a spring to the scroll progress
const scaleX = useSpring(scrollYProgress, {
stiffness: 100,
damping: 30,
restDelta: 0.001
});
return (
<motion.div
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
height: '10px',
background: 'purple',
transformOrigin: '0%',
scaleX // Use the spring-animated value
}}
/>
);
}
In this example, the progress bar will now have a slight “lag” and “overshoot” as you scroll quickly, creating a much more pleasing and physical effect. This technique is often seen in high-end digital experiences built with tools like Remix News or Blitz.js News.
Animating Based on Scroll Velocity

For even more dynamic effects, you can use the useVelocity
hook to track the rate of change of a MotionValue
. For example, you could make an element skew or stretch based on how fast the user is scrolling, adding a playful and responsive feel to the interaction.
Let’s create an element that skews based on scroll velocity.
import { motion, useScroll, useTransform, useSpring, useVelocity } from 'framer-motion';
export default function VelocitySkew() {
const { scrollY } = useScroll();
// Get the velocity of the scrollY MotionValue
const scrollYVelocity = useVelocity(scrollY);
// Map velocity to a skew value. A higher velocity results in a greater skew.
// The range [-1000, 1000] is an example; you'd adjust this based on testing.
const skew = useTransform(scrollYVelocity, [-1000, 1000], [-15, 15]);
// We apply a spring to the skew to smooth it out
const smoothSkew = useSpring(skew, { stiffness: 200, damping: 25 });
return (
<div style={{ height: '200vh', paddingTop: '100px', textAlign: 'center' }}>
<motion.h1 style={{ skew: smoothSkew }}>
Scroll Fast!
</motion.h1>
</div>
);
}
This code introduces a new level of interactivity. The h1
element will remain upright when scrolling slowly but will skew left or right when the user scrolls quickly up or down. This kind of direct feedback on user input is a hallmark of excellent interaction design. Such advanced state-derived animations can also be integrated with state management libraries like Zustand News or Redux News if the animation needs to be influenced by global application state.
Section 4: Best Practices and Performance Optimization
While Framer Motion is highly optimized, scroll-linked animations can be performance-intensive if not handled carefully. The browser has to recalculate styles and repaint on every single scroll event, which can lead to jank or lag, especially on less powerful devices. Following best practices is crucial for ensuring your animations are smooth and efficient.
1. Animate Only `transform` and `opacity`
The golden rule of performant web animations is to stick to properties that can be hardware-accelerated by the GPU. These are primarily transform
(which includes translateX
, translateY
, scale
, rotate
, skew
) and opacity
. Animating these properties does not trigger a browser reflow or repaint of the document layout, making them incredibly cheap to update. Avoid animating properties like width
, height
, margin
, or top
/left
, as they force the browser to recalculate the layout of the page, which is a very expensive operation.
2. Scope `useScroll` with a Target Ref

Whenever possible, track the scroll progress of a specific container element rather than the entire viewport. By providing a target
ref to the useScroll
hook, you limit the scope of its calculations. This is more efficient than tracking the global window
scroll, especially on long, complex pages.
3. Consider Accessibility
Animations can be problematic for users with vestibular disorders or motion sensitivities. Always respect the prefers-reduced-motion
media query. You can use a simple hook to check for this preference and conditionally disable or reduce your animations.
import { useReducedMotion } from 'framer-motion';
function MyAnimatedComponent() {
const shouldReduceMotion = useReducedMotion();
// Conditionally apply animations
const scale = shouldReduceMotion ? 1 : useTransform(...);
return <motion.div style={{ scale }} />;
}
4. Isolate and Test Your Components
Developing complex animations in isolation is key to getting them right. Tools like Storybook News are invaluable for this, allowing you to build and test your animated components without the noise of the full application. For integration and end-to-end testing, frameworks like Cypress News and Playwright News can help you verify that your scroll-based interactions behave as expected, while React Testing Library News is excellent for unit-level testing of component logic.
Conclusion
Framer Motion provides a remarkably powerful and intuitive API for creating stunning scroll-based animations in React. By leveraging the core hooks—useScroll
, useTransform
, and useSpring
—developers can move beyond static layouts and build dynamic, engaging, and narrative-driven web experiences. We’ve journeyed from the fundamental concepts of MotionValue
s to implementing practical scaling and rotating effects, and even explored advanced techniques like spring physics and velocity tracking.
The key takeaways are to embrace the declarative nature of the library, always prioritize performance by animating `transform` and `opacity`, and be mindful of accessibility. As you continue your journey, experiment with combining different transformations, explore other hooks like useInView
for simpler trigger-once animations, and see how these patterns can enhance your projects built with modern frameworks like Next.js News or Vite News. By mastering these techniques, you can add a layer of professional polish that will captivate your users and set your applications apart.