Introduction
Scroll-driven animations tie visual effects to scroll position — parallax backgrounds, progress indicators, reveal-on-scroll elements, and sticky transformations. They create engaging, interactive experiences that respond to user movement.
The new CSS Scroll-Driven Animations API (scroll-timeline) enables performant scroll animations without JavaScript. Combined with Intersection Observer for triggering and Framer Motion for React integration, you have a complete toolkit.
This guide covers modern scroll animation techniques from CSS-only solutions to advanced JavaScript implementations.
Key Concepts
CSS scroll-timeline
/* Animate progress bar based on scroll */
@keyframes grow {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
.progress-bar {
animation: grow linear;
animation-timeline: scroll();
transform-origin: left;
}
Intersection Observer for Reveals
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
}
});
},
{ threshold: 0.1 }
);
document.querySelectorAll('.reveal').forEach(el => observer.observe(el));
View Timeline
/* Animate as element enters/exits viewport */
.card {
animation: fadeIn linear both;
animation-timeline: view();
animation-range: entry 0% entry 100%;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(50px); }
to { opacity: 1; transform: translateY(0); }
}
Practical Examples
1. Scroll Progress Indicator
.scroll-progress {
position: fixed; top: 0; left: 0;
width: 100%; height: 3px;
background: #3b82f6;
transform-origin: left;
animation: grow linear;
animation-timeline: scroll();
z-index: 100;
}
@keyframes grow { from { transform: scaleX(0); } to { transform: scaleX(1); } }
2. Parallax Effect
import { useScroll, useTransform, motion } from 'framer-motion';
function Parallax() {
const { scrollYProgress } = useScroll();
const y = useTransform(scrollYProgress, [0, 1], [0, -300]);
return (
<div style={{ position: 'relative', height: '200vh' }}>
<motion.div style={{ y, position: 'fixed' }}>
<img src="/bg.jpg" alt="Background" />
</motion.div>
</div>
);
}
3. Reveal on Scroll (CSS only)
.reveal {
opacity: 0; transform: translateY(30px);
transition: opacity 0.6s ease, transform 0.6s ease;
}
.reveal.visible {
opacity: 1; transform: translateY(0);
}
4. Horizontal Scroll Section
.horizontal-scroll {
display: flex; overflow-x: auto;
scroll-snap-type: x mandatory;
}
.horizontal-scroll > * {
flex: 0 0 100vw;
scroll-snap-align: start;
}
Best Practices
- ✅ Use CSS scroll-timeline for simple scroll-linked animations
- ✅ Use Intersection Observer for scroll-triggered one-time animations
- ✅ Animate only transform and opacity for 60fps
- ✅ Add will-change: transform to scrolling elements
- ✅ Respect prefers-reduced-motion
- ❌ Don't use scroll event listeners for animations — use requestAnimationFrame or CSS
- ❌ Don't create motion sickness with excessive parallax
Common Pitfalls
- 🚫 Using scroll event without throttling — causes jank
- 🚫 Too much parallax — causes motion sickness for some users
- 🚫 Not checking browser support for scroll-timeline
- 🚫 Heavy animations triggered on every scroll frame