Introduction
Scrollytelling is a visual storytelling technique that uses scroll to progressively reveal a story or information. Combined with the CSS property position: sticky, it allows creating immersive web experiences worthy of the greatest editorial sites like the New York Times or Bloomberg.
In this complete guide, we will explore in depth the fundamental and advanced techniques for mastering these effects. You will learn to use position sticky, the Intersection Observer API, and even the new native CSS scroll-driven animations.
Studies show that interactive content with scrollytelling has a 3x higher engagement rate than static content. It is particularly effective for data visualization and long articles.
Position sticky explained
The position: sticky property is the foundation of many scrollytelling effects. It allows an element to switch between a relative and fixed behavior depending on the scroll position.
How it works
A sticky element starts by behaving like a relatively positioned element. Then, when it reaches a defined threshold (e.g. top: 0), it becomes "stuck" at that position like a fixed element, until its parent container scrolls out of view.
Scroll down to see the sticky effect in action. The purple element stays stuck at the top of the container as you scroll.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Keep scrolling to see the sticky element follow the scroll until the end of the container.
/* Container that defines the scroll area */
.sticky-container {
position: relative;
height: 300vh; /* Large height for scrolling */
}
/* Element that stays stuck */
.sticky-element {
position: sticky;
top: 20px; /* Distance from top */
background: linear-gradient(135deg, #6366f1, #8b5cf6);
padding: 24px 32px;
border-radius: 12px;
}
Important points
- The parent must have a height: sticky only works if the parent container has enough height to allow scrolling
- No overflow: hidden on ancestors: this property can break sticky behavior
- Specify top, bottom, left or right: at least one of these properties must be defined
- Works with all modern browsers: 95%+ support without prefix
If your sticky element is not working, check that you don't have overflow: hidden, overflow: auto or overflow: scroll on a parent element between the sticky and the viewport.
Scrollytelling with Intersection Observer
The Intersection Observer API detects when an element enters or exits the viewport (or another element). It is the ideal tool for triggering animations or state changes on scroll.
Demo interactive
Scroll in the right area to see the visual change based on the active step:
Step 1: Introduction
The circle starts in its initial shape, with a classic purple gradient.
Step 2: Transformation
The circle grows and takes on a rounded square shape with a new gradient.
Step 3: Rotation
The element rotates and changes color to cyan-green tones.
Step 4: Conclusion
Back to a circular shape with warm orange-red colors.
// Element selection
const steps = document.querySelectorAll('.scroll-step');
const visual = document.querySelector('.visual-circle');
// Observer configuration
const observerOptions = {
root: document.querySelector('.scrollytelling-steps'),
rootMargin: '-30% 0px -30% 0px', // Detection zone
threshold: 0
};
// Observer callback
const observerCallback = (entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
// Get the step number
const step = entry.target.dataset.step;
// Update the visual
visual.setAttribute('data-step', step);
visual.textContent = step;
// Update active classes
steps.forEach(s => s.classList.remove('active'));
entry.target.classList.add('active');
}
});
};
// Create and activate the observer
const observer = new IntersectionObserver(observerCallback, observerOptions);
steps.forEach(step => observer.observe(step));
Understanding the options
- root: the reference element (null = viewport)
- rootMargin: margins to extend or shrink the detection area
- threshold: visibility percentage to trigger the callback (0 to 1)
Native CSS scroll-timeline
Scroll-driven animations are a new CSS specification that allows linking animations directly to scroll, without JavaScript! It is the most performant method as everything runs on the GPU.
In 2025, scroll-timeline is supported by Chrome, Edge and Opera. Firefox and Safari are in the process of implementing it. Use feature detection for the fallback.
/* Scroll timeline definition */
@scroll-timeline scroll-progress {
source: auto; /* Element scrollable (auto = ancetre) */
orientation: vertical;
scroll-offsets: 0%, 100%;
}
/* Scroll-linked animation */
.progress-bar {
animation: fillProgress linear;
animation-timeline: scroll-progress;
}
@keyframes fillProgress {
from { width: 0%; }
to { width: 100%; }
}
Simplified new syntax
The specification has evolved toward a simpler syntax with animation-timeline: scroll():
/* Progress bar linked to page scroll */
.reading-progress {
position: fixed;
top: 0;
left: 0;
height: 4px;
background: linear-gradient(90deg, #6366f1, #8b5cf6);
transform-origin: left;
/* Scroll-linked animation */
animation: scaleProgress linear forwards;
animation-timeline: scroll(root);
}
@keyframes scaleProgress {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
/* Animation linked to element visibility (view timeline) */
.fade-in-element {
animation: fadeIn linear forwards;
animation-timeline: view();
animation-range: entry 0% cover 40%;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(50px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
Advanced practical examples
Let's now look at concrete and more elaborate use cases.
1. Reading progress indicator
A bar that shows where you are in reading the article:
// Reading progress indicator
function initReadingProgress() {
const progressBar = document.querySelector('.progress-fill');
const article = document.querySelector('article');
function updateProgress() {
// Calculate scroll position
const scrollTop = window.scrollY;
const docHeight = article.offsetHeight - window.innerHeight;
const progress = (scrollTop / docHeight) * 100;
// Clamp between 0 and 100
const clampedProgress = Math.min(100, Math.max(0, progress));
// Update the width
progressBar.style.width = clampedProgress + '%';
}
// Listen to scroll with throttling
let ticking = false;
window.addEventListener('scroll', () => {
if (!ticking) {
requestAnimationFrame(() => {
updateProgress();
ticking = false;
});
ticking = true;
}
});
}
2. Animated vertical timeline
A timeline component that animates as the user scrolls:
Phase 1: Research
User needs analysis
Phase 2: Design
UI/UX mockup creation
Phase 3: Development
Implementation technique
Phase 4: Launch
Deploiement en production
<!-- Timeline HTML structure -->
<div class="timeline-track">
<div class="timeline-item">
<div class="timeline-dot"></div>
<div class="timeline-content">
<h4>Step title</h4>
<p>Step description</p>
</div>
</div>
<!-- Autres items... -->
</div>
<style>
.timeline-track {
position: relative;
padding-left: 40px;
}
/* Ligne verticale */
.timeline-track::before {
content: '';
position: absolute;
left: 12px;
top: 0;
bottom: 0;
width: 2px;
background: rgba(255,255,255,0.1);
}
.timeline-dot {
position: absolute;
left: -34px;
width: 16px;
height: 16px;
border-radius: 50%;
background: #12121a;
border: 2px solid rgba(255,255,255,0.1);
transition: all 0.3s ease;
}
/* Active state */
.timeline-item.active .timeline-dot {
background: #6366f1;
border-color: #6366f1;
box-shadow: 0 0 20px rgba(99,102,241,0.5);
}
</style>
3. Parallax with sticky
Combining sticky with transformations to create a parallax effect:
/* Section with sticky parallax */
.parallax-section {
height: 200vh;
position: relative;
}
.parallax-background {
position: sticky;
top: 0;
height: 100vh;
overflow: hidden;
}
.parallax-image {
width: 100%;
height: 120%;
object-fit: cover;
will-change: transform;
}
.parallax-content {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 100vh 20px 50px;
background: linear-gradient(
to bottom,
transparent,
rgba(10,10,15,0.8) 30%,
#0a0a0f
);
}
Best practices
To create performant and accessible scrollytelling experiences, follow these recommendations:
Performance
- Use requestAnimationFrame for scroll-linked animations in JavaScript
- Prefer transform and opacity for animations (properties that do not trigger reflow)
- Enable will-change sparingly on animated elements
- Prefer CSS scroll-timeline when possible (GPU rendering)
- Limit the number of observers: a single observer can watch multiple elements
// Optimization with requestAnimationFrame
let rafId = null;
let lastScrollY = 0;
function onScroll() {
lastScrollY = window.scrollY;
if (!rafId) {
rafId = requestAnimationFrame(updateAnimations);
}
}
function updateAnimations() {
// Your animations here
// Use transform instead of top/left
element.style.transform = `translateY(${lastScrollY * 0.5}px)`;
rafId = null;
}
window.addEventListener('scroll', onScroll, { passive: true });
Accessibility
- Respect prefers-reduced-motion: disable or reduce animations
- Content must be readable without JS: progressive enhancement
- Test with keyboard: focus must follow visible content
- Use aria-live for important dynamic changes
/* Disable animations for sensitive users */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
/* Sticky elements remain functional */
.sticky-element {
position: sticky; /* conserve */
}
}
UX and Design
- Give visual cues: the user must understand they can scroll
- Avoid scroll jacking: do not alter the native scroll speed
- Test on mobile: behavior can differ significantly
- Plan fallbacks: content must be accessible without the effects
Use DevTools to profile your animations. In Chrome, open Performance > Record while scrolling, then analyze the "Frames" to detect FPS drops.
Conclusion
Scrollytelling and sticky sections are powerful tools for creating memorable web experiences. By combining position: sticky, the Intersection Observer and the new scroll-driven animations, you have everything you need to compete with the best editorial sites.
Key takeaways:
- position: sticky is the foundation of many scroll effects
- Intersection Observer allows triggering actions without impacting performance
- CSS scroll-timeline is the future of scroll-linked animations
- Performance and accessibility must always be top priorities
Do not hesitate to experiment and combine these techniques. The possibilities are endless!
Discover our ready-to-use scrollytelling templates in the Effect.Labs library, with sophisticated effects and optimized code.