Skip to content
CSS

Scroll-Driven Animations

Create performant scroll-driven animations with CSS and JavaScript for engaging user experiences.

stellae.design

8 min read

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

css
/* 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

javascript
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

css
/* 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

css
.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

typescript
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)

css
.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

css
.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

CSScss

Was this article helpful?