Introduction
Scroll-triggered animations have become essential for creating memorable web experiences. They guide the user's attention, tell a visual story, and add an interactive dimension to your pages.
In this complete guide, we'll explore 5 essential techniques for implementing scroll-driven animations, from the most classic (Intersection Observer) to the most modern (CSS scroll-timeline). Each technique comes with an interactive demo and the complete code.
The progress bar at the top of the page and the reading tracker in the table of contents are concrete examples of scroll-driven animations. Scroll to see them in action!
1. Scroll fade-in with Intersection Observer
The Intersection Observer API is the most reliable and performant method for detecting when an element enters the viewport. Unlike older techniques based on scroll events, it does not block the main thread.
Rapide
Smooth animation
Performant
Zero jank
Compatible
All browsers
// Observer configuration
const observerOptions = {
root: null, // default viewport
rootMargin: '0px', // margin around viewport
threshold: 0.1 // 10% visible to trigger
};
// Callback when an element enters/exits the viewport
const handleIntersect = (entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
// Element visible: add class
entry.target.classList.add('visible');
// Optional: stop observing after animation
observer.unobserve(entry.target);
}
});
};
// Create the observer
const observer = new IntersectionObserver(handleIntersect, observerOptions);
// Observe all elements with [data-animate]
document.querySelectorAll('[data-animate]').forEach(el => {
observer.observe(el);
});
/* Initial state: invisible and shifted down */
.fade-card {
opacity: 0;
transform: translateY(30px);
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Final state: visible and in place */
.fade-card.visible {
opacity: 1;
transform: translateY(0);
}
/* Cascade delay for stagger effect */
.fade-card:nth-child(2) { transition-delay: 0.1s; }
.fade-card:nth-child(3) { transition-delay: 0.2s; }
.fade-card:nth-child(4) { transition-delay: 0.3s; }
Key Intersection Observer parameters
- root: The parent element serving as the viewport (null = browser viewport)
- rootMargin: Margin around the root (e.g., "-100px" to trigger 100px before)
- threshold: Required visibility percentage (0.1 = 10%, 1 = 100%)
2. Parallax effect
The parallax effect creates an illusion of depth by moving different layers at different speeds while scrolling. It's a classic of modern web design.
// Elements with different parallax speeds
const layers = [
{ element: document.querySelector('.parallax-stars'), speed: 0.2 },
{ element: document.querySelector('.parallax-moon'), speed: 0.3 },
{ element: document.querySelector('.mountain-1'), speed: 0.5 },
{ element: document.querySelector('.mountain-2'), speed: 0.7 },
];
// Optimized function with requestAnimationFrame
let ticking = false;
function updateParallax() {
const scrollY = window.scrollY;
layers.forEach(layer => {
const yOffset = scrollY * layer.speed;
layer.element.style.transform = `translateY(${yOffset}px)`;
});
ticking = false;
}
window.addEventListener('scroll', () => {
if (!ticking) {
window.requestAnimationFrame(updateParallax);
ticking = true;
}
});
Always use requestAnimationFrame for scroll-linked animations. Without this optimization, you risk jank that degrades the user experience.
3. Reading progress bar
The reading progress bar is an excellent visual indicator for long articles. It shows the user where they are in their reading.
const progressBar = document.querySelector('.reading-progress');
const article = document.querySelector('.article-content');
function updateProgress() {
// Article position
const articleTop = article.getBoundingClientRect().top + window.scrollY;
const articleHeight = article.offsetHeight;
const windowHeight = window.innerHeight;
// Percentage calculation
const scrolled = window.scrollY - articleTop + windowHeight;
const total = articleHeight + windowHeight;
const progress = Math.min(Math.max(scrolled / total, 0), 1);
// Update the progress bar
progressBar.style.width = `${progress * 100}%`;
}
// Listen to scroll with throttling
let rafId = null;
window.addEventListener('scroll', () => {
if (rafId) return;
rafId = requestAnimationFrame(() => {
updateProgress();
rafId = null;
});
});
.reading-progress {
position: fixed;
top: 0;
left: 0;
width: 0%;
height: 3px;
background: linear-gradient(
90deg,
#6366f1,
#8b5cf6,
#d946ef
);
z-index: 9999;
transition: width 0.1s ease-out;
}
4. Reveal animations (slide, scale)
Reveal animations make elements appear in different ways: from the left, right, with a zoom effect, or even with a rotation. Here is an example with several variants.
Scale
Zoom from the center
Rotate
Rotation + scale
/* Slide from the left */
.reveal-slide-left {
opacity: 0;
transform: translateX(-50px);
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Slide from the right */
.reveal-slide-right {
opacity: 0;
transform: translateX(50px);
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Scale from center */
.reveal-scale {
opacity: 0;
transform: scale(0.8);
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Rotation + scale */
.reveal-rotate {
opacity: 0;
transform: rotate(-10deg) scale(0.9);
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Common visible state */
.reveal-slide-left.visible,
.reveal-slide-right.visible,
.reveal-scale.visible,
.reveal-rotate.visible {
opacity: 1;
transform: translateX(0) scale(1) rotate(0);
}
Staggered animation (stagger)
To create an elegant stagger effect, use transition-delay with an incremental value:
// Add incremental delay to each element
const items = document.querySelectorAll('[data-animate="reveal"]');
items.forEach((item, index) => {
item.style.transitionDelay = `${index * 0.1}s`;
});
5. Native CSS scroll-timeline
The CSS Scroll-driven Animations specification allows animations to be linked directly to scroll progress, without JavaScript. It is the most performant method as it runs entirely on the GPU.
Scroll-linked animation
Scroll the page to see the transformation
/* Animation keyframes classique */
@keyframes scrollReveal {
from {
opacity: 0;
transform: translateY(100px) scale(0.8);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.scroll-animated-element {
/* Link animation to page scroll */
animation: scrollReveal linear;
animation-timeline: scroll();
/* Control when the animation plays */
animation-range: entry 0% cover 50%;
}
animation-timeline options
scroll(): Linked to the document scroll timelinescroll(root): Linked to the root document scrollscroll(nearest): Linked to the nearest scrollable containerview(): Linked to the element's visibility in the viewport
animation-range options
entry: When the element enters the viewportexit: When the element exits the viewportcover: When the element covers the viewportcontain: When the element is fully visible
Scroll-timeline is supported in Chrome 115+ and Edge 115+. For broader compatibility, use Intersection Observer with a fallback.
Best practices and accessibility
Performance
- Use
transformandopacity: These are composite properties animated on the GPU - Avoid animating
width,height,top,left: They trigger costly reflows - Throttling with
requestAnimationFrame: Essential for scroll-linked animations will-changesparingly: Useful but consumes GPU memory
Accessibility
Always respect user preferences regarding animations:
/* Disable animations if user requests it */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
/* Display animated elements directly */
.fade-card,
.reveal-item {
opacity: 1 !important;
transform: none !important;
}
}
// Check user preference in JavaScript
const prefersReducedMotion = window.matchMedia(
'(prefers-reduced-motion: reduce)'
);
if (prefersReducedMotion.matches) {
// Disable JavaScript animations
console.log('Animations reduites');
}
// Listen for preference changes
prefersReducedMotion.addEventListener('change', () => {
// Dynamically enable/disable
});
Techniques comparison table
| Technique | Performance | Compatibility | Use case |
|---|---|---|---|
| Intersection Observer | Excellent | 97%+ | Reveal, lazy loading |
| Scroll event + rAF | Good | 100% | Parallax, progress bar |
| CSS scroll-timeline | Optimal | ~75% | Complex animations |
Conclusion
Scroll-driven animations are a powerful tool for creating immersive web experiences. With the 5 techniques presented in this guide, you have everything you need to implement performant and accessible animations.
Remember these essential points:
- Intersection Observer for simple reveal animations
- requestAnimationFrame for parallax and progress bars
- CSS scroll-timeline for complex animations (with fallback)
- Always respect
prefers-reduced-motion
Find dozens of scroll effects ready to use in our effects library, with copyable and customizable code.