Live demo
Scroll animations trigger or respond to the user’s scroll position, creating dynamic effects that enhance storytelling and user engagement. They range from simple fade-in-on-scroll to complex parallax and scroll-driven narratives.
Scroll animations should enhance content, not obstruct it. Always ensure content remains accessible even if animations fail.
Complete code example
How it works
Scroll animations use the scroll position to drive animations:
Track scroll position
Use useScroll() to get scroll progress values (0 to 1).
Transform values
Convert scroll progress to animation values using useTransform().
Apply to elements
Link transformed values to element properties via the style prop.
Add smoothing
Use useSpring() to smooth jerky scroll movements.
useScroll Tracks scroll position and returns progress values
useInView Detects when elements enter/leave the viewport
useTransform Maps scroll values to animation properties
useSpring Adds smooth spring physics to scroll values
Variations
Reveal items with delay as they enter view:
import { motion } from 'framer-motion'
import { useInView } from 'framer-motion'
import { useRef } from 'react'
function StaggeredReveal ({ items }) {
const ref = useRef ( null )
const isInView = useInView ( ref , { once: true })
return (
< div ref = { ref } >
{ items . map (( item , i ) => (
< motion.div
key = { i }
initial = { { opacity: 0 , x: - 50 } }
animate = { isInView ? { opacity: 1 , x: 0 } : { opacity: 0 , x: - 50 } }
transition = { { delay: i * 0.1 , duration: 0.5 } }
className = "p-4 mb-4 bg-white rounded-lg shadow"
>
{ item }
</ motion.div >
)) }
</ div >
)
}
Animate numbers based on scroll:
import { motion , useScroll , useTransform , useSpring } from 'framer-motion'
import { useRef , useEffect , useState } from 'react'
function ScrollCounter ({ target = 100 }) {
const ref = useRef ( null )
const { scrollYProgress } = useScroll ({
target: ref ,
offset: [ 'start end' , 'end start' ]
})
const count = useTransform ( scrollYProgress , [ 0 , 1 ], [ 0 , target ])
const springCount = useSpring ( count , { stiffness: 100 , damping: 30 })
const [ displayCount , setDisplayCount ] = useState ( 0 )
useEffect (() => {
return springCount . onChange ( v => setDisplayCount ( Math . round ( v )))
}, [])
return (
< div ref = { ref } className = "h-screen flex items-center justify-center" >
< motion.div className = "text-6xl font-bold text-blue-500" >
{ displayCount }
</ motion.div >
</ div >
)
}
Scroll horizontally through sections:
import { motion , useScroll , useTransform } from 'framer-motion'
import { useRef } from 'react'
function HorizontalScroll () {
const ref = useRef ( null )
const { scrollYProgress } = useScroll ({ target: ref })
const x = useTransform ( scrollYProgress , [ 0 , 1 ], [ '0%' , '-75%' ])
return (
< div ref = { ref } className = "h-[400vh] relative" >
< div className = "sticky top-0 h-screen overflow-hidden" >
< motion.div style = { { x } } className = "flex h-full" >
{ [ 1 , 2 , 3 , 4 ]. map (( i ) => (
< div
key = { i }
className = "min-w-full h-full flex items-center justify-center bg-gradient-to-r from-blue-500 to-purple-500 text-white text-6xl font-bold"
>
Section { i }
</ div >
)) }
</ motion.div >
</ div >
</ div >
)
}
Reveal image as user scrolls:
import { motion , useScroll , useTransform } from 'framer-motion'
import { useRef } from 'react'
function ImageReveal ({ src }) {
const ref = useRef ( null )
const { scrollYProgress } = useScroll ({
target: ref ,
offset: [ 'start end' , 'end start' ]
})
const scale = useTransform ( scrollYProgress , [ 0 , 0.5 , 1 ], [ 0.8 , 1 , 0.8 ])
const opacity = useTransform ( scrollYProgress , [ 0 , 0.3 , 0.7 , 1 ], [ 0 , 1 , 1 , 0 ])
return (
< div ref = { ref } className = "h-screen flex items-center justify-center" >
< motion.img
src = { src }
style = { { scale , opacity } }
className = "max-w-2xl rounded-lg shadow-2xl"
/>
</ div >
)
}
Example from the playground
From the ReactAnimations component:
import { useScroll , useSpring , motion } from 'framer-motion'
function ScrollAnimation () {
const { scrollYProgress } = useScroll ()
const scaleX = useSpring ( scrollYProgress , {
stiffness: 100 ,
damping: 30 ,
restDelta: 0.001
})
return (
< motion.div
style = { { scaleX } }
className = "fixed top-0 left-0 right-0 h-1 bg-blue-500 origin-left"
/>
)
}
Best practices
Use 'once' for entrance animations
Prevent elements from re-animating when scrolling back up. const isInView = useInView ( ref , { once: true })
Trigger animations slightly before elements enter viewport. const isInView = useInView ( ref , { margin: '-100px' })
Apply spring physics to scroll-driven values for smoother motion. const smoothProgress = useSpring ( scrollYProgress , {
stiffness: 100 ,
damping: 30
})
Disable scroll animations for users with motion sensitivity. const prefersReducedMotion = window . matchMedia (
'(prefers-reduced-motion: reduce)'
). matches
Common use cases
Scroll progress indicators
Fade-in content on scroll
Parallax backgrounds
Sticky headers with animations
Horizontal scrolling galleries
Scroll-driven counters
Image reveals
Section transitions
Scroll animations can impact performance on lower-end devices. Test thoroughly and provide fallbacks.
// Good - GPU accelerated
< motion.div style = { { y , opacity } } />
// Avoid - causes layout recalc
< motion.div style = { { top: scrollY , height: scrollHeight } } />
Animate based on specific section visibility:
import { motion , useScroll , useTransform } from 'framer-motion'
import { useRef } from 'react'
function SectionScroll () {
const ref = useRef ( null )
const { scrollYProgress } = useScroll ({
target: ref ,
offset: [ 'start end' , 'end start' ]
})
const backgroundColor = useTransform (
scrollYProgress ,
[ 0 , 0.5 , 1 ],
[ '#3B82F6' , '#8B5CF6' , '#EC4899' ]
)
return (
< motion.div
ref = { ref }
style = { { backgroundColor } }
className = "h-screen flex items-center justify-center text-white text-4xl font-bold"
>
Color changes as you scroll
</ motion.div >
)
}
Fade in Basic opacity transitions
Drag gesture Interactive drag animations
Slide in Directional animations