Blog / CSS/JS

Sticky Sections and Scrollytelling: The Complete Guide

Master sticky positioning, Intersection Observer and scroll-timeline techniques to create narrative and immersive scroll experiences that will captivate your users.

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.

💡
Why scrollytelling?

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.

I stay stuck at the top!

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.

sticky-basic.css
/* 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
⚠️
Common pitfall

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:

1

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.

scrollytelling.js
// 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.

📊
Browser support

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.css
/* 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():

scroll-animation-modern.css
/* 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:

Scroll this page to see the progress
reading-progress.js
// 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

animated-timeline.html
<!-- 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:

sticky-parallax.css
/* 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
performance-tips.js
// 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
accessibility.css
/* 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
💡
Pro tip

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!

🎨
Go further

Discover our ready-to-use scrollytelling templates in the Effect.Labs library, with sophisticated effects and optimized code.