Skip to main content
โšก Calmops

CSS Scroll-Driven Animations: A Complete Guide

CSS Scroll-driven Animations represent a revolutionary way to create scroll-based animations using only CSS. No more JavaScript scroll listeners or Intersection Observer - now you can create fluid, performant scroll animations directly in your stylesheets.

What are Scroll-Driven Animations?

Scroll-driven animations link animation progress to scroll position. As the user scrolls, the animation progresses automatically. This enables effects like:

  • Parallax effects
  • Progress indicators
  • Reveal animations
  • Sticky headers with transitions
  • Reading progress bars
/* Link animation to scroll progress */
@keyframes fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

.hero {
  animation: fade-in linear;
  animation-timeline: scroll();
}

The Problem: JavaScript Scroll Animations

Before scroll-driven animations, achieving scroll-based effects required JavaScript:

The Old Approach: JavaScript

// Traditional scroll-driven animation with JavaScript
window.addEventListener('scroll', () => {
  const scrollPosition = window.scrollY;
  const element = document.querySelector('.hero');
  const progress = scrollPosition / 500;
  element.style.opacity = Math.min(progress, 1);
});

Problems with this approach:

  • Performance issues from scroll event listeners
  • Requires manual calculation of scroll progress
  • Complex to handle different scroll containers
  • janky animations on some browsers

The Modern Approach: Pure CSS

/* Same effect with CSS Scroll-driven Animation */
@keyframes fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

.hero {
  animation: fade-in linear;
  animation-timeline: scroll();
}

animation-timeline Property

The animation-timeline property controls what the animation is tied to:

/* Available values */
animation-timeline: scroll();           /* Document scroll */
animation-timeline: view();            /* Element's visibility in viewport */
animation-timeline: view(block);        /* Vertical scroll */
animation-timeline: view(inline);       /* Horizontal scroll */
animation-timeline: none;              /* No scroll-linked */

scroll() Function

The scroll() function creates a timeline based on scroll position:

/* Default: scroll from root */
animation-timeline: scroll();

/* Scroll in specific root */
animation-timeline: scroll(nearest);
animation-timeline: scroll(root);
animation-timeline: scroll(self);
/* Horizontal scroll */
animation-timeline: scroll(inline);

view() Function

The view() function creates a timeline based on an element’s visibility in its scroll container:

/* Default: block axis (vertical) */
animation-timeline: view();

/* Inline axis (horizontal) */
animation-timeline: view(inline);
/* With margin - animation starts/ends with margin */
animation-timeline: view(20px);
animation-timeline: view(0px 100px); /* top bottom */

Basic Examples

Example 1: Reading Progress Bar

/* Progress bar that fills as you scroll */
@keyframes progress-fill {
  from { width: 0%; }
  to { width: 100%; }
}

.progress-bar {
  position: fixed;
  top: 0;
  left: 0;
  height: 4px;
  background: blue;
  
  animation: progress-fill linear;
  animation-timeline: scroll();
}
<!-- Full page progress indicator -->
<div class="progress-bar"></div>
<article>
  <h1>Long Article</h1>
  <p>Content...</p>
</article>

Example 2: Reveal on Scroll

/* Elements fade in as they enter viewport */
@keyframes reveal {
  from {
    opacity: 0;
    transform: translateY(50px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.reveal-item {
  animation: reveal ease-out;
  animation-timeline: view();
  animation-range: entry 0% cover 40%;
}
<section class="content">
  <div class="reveal-item">Content 1</div>
  <div class="reveal-item">Content 2</div>
  <div class="reveal-item">Content 3</div>
</section>

Example 3: Parallax Effect

/* Simple parallax with scroll timeline */
@keyframes parallax {
  from { transform: translateY(0); }
  to { transform: translateY(-50%); }
}

.parallax-bg {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  z-index: -1;
  
  animation: parallax linear;
  animation-timeline: scroll();
}

animation-range Property

The animation-range property controls when the animation starts and ends relative to the scroll position:

/* Basic usage */
animation-range: start end;

/* Common patterns */
animation-range: 0% 100%;      /* Full scroll */
animation-range: entry 0% cover 100%;  /* Entry to exit */
animation-range: 0% cover 50%; /* First half */
animation-range: contain 0% contain 100%; /* While contained */

Range Keywords

/* Named range positions */
animation-range: entry 0%;      /* Element enters viewport */
animation-range: entry 100%;    /* Element fully entered */
animation-range: exit 0%;       /* Element starts exiting */
animation-range: exit 100%;     /* Element fully exited */
animation-range: cover 0%;       /* Covers full range */
animation-range: contain 0%;    /* While contained */
/* Practical examples */
animation-range: entry 0% cover 30%;   /* Animate during first 30% */
animation-range: cover 50% exit 50%;   /* Middle 50% */
animation-range: entry 25% exit 75%;   /* Animate in middle portion */

Complex Animations

Example 1: Image Sequence Animation

/* Animate through frames while scrolling */
@keyframes frame-sequence {
  from { background-position: 0 0; }
  to { background-position: -5000px 0; }
}

.frame-animation {
  width: 100%;
  height: 500px;
  background-image: url('/spritesheet.png');
  background-repeat: no-repeat;
  
  animation: frame-sequence linear;
  animation-timeline: scroll();
  animation-range: 0% 100%;
}

Example 2: Sticky Header Transformation

/* Header shrinks and changes color on scroll */
@keyframes header-transform {
  from {
    padding: 2rem;
    background: transparent;
  }
  to {
    padding: 0.5rem;
    background: white;
    box-shadow: 0 2px 10px rgba(0,0,0,0.1);
  }
}

.sticky-header {
  position: sticky;
  top: 0;
  
  animation: header-transform linear;
  animation-timeline: scroll(root);
  animation-range: 0 200px;
}

Example 3: Section-based Navigation

/* Update navigation active state based on scroll */
@keyframes nav-highlight {
  from, to { background: transparent; }
  25% { background: active; }
}

nav li:nth-child(1) { animation: nav-highlight linear; animation-timeline: view(section1); }
nav li:nth-child(2) { animation: nav-highlight linear; animation-timeline: view(section2); }
nav li:nth-child(3) { animation: nav-highlight linear; animation-timeline: view(section3); }

Example 4: Circular Progress

/* Circular progress indicator tied to scroll */
@keyframes circular-progress {
  from { stroke-dashoffset: 100; }
  to { stroke-dashoffset: 0; }
}

.progress-circle {
  animation: circular-progress linear;
  animation-timeline: scroll();
}
<svg class="progress-circle" viewBox="0 0 100 100">
  <circle cx="50" cy="50" r="45" stroke-dasharray="283" />
</svg>

Scroll Container Animations

Horizontal Scrolling

/* Animate based on horizontal scroll */
@keyframes slide-in {
  from { transform: translateX(-100%); }
  to { transform: translateX(0); }
}

.horizontal-scroll-item {
  animation: slide-in ease-out;
  animation-timeline: scroll(inline);
  animation-range: entry 0% cover 40%;
}
<div class="horizontal-container">
  <div class="horizontal-scroll-item">Item 1</div>
  <div class="horizontal-scroll-item">Item 2</div>
  <div class="horizontal-scroll-item">Item 3</div>
</div>
/* CSS for horizontal container */
.horizontal-container {
  display: flex;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
}

Nested Scroll Containers

/* Animation tied to specific scroll container */
.sidebar {
  overflow-y: auto;
  height: 100vh;
}

.sidebar-item {
  animation: fade-in ease-out;
  animation-timeline: scroll(self);
}

Combining with Other CSS Features

With View Transitions

/* Smooth scroll-driven animations with view transitions */
@keyframes scale-in {
  from { transform: scale(0.8); opacity: 0; }
  to { transform: scale(1); opacity: 1; }
}

.card {
  animation: scale-in ease-out;
  animation-timeline: view();
  animation-range: entry 0% cover 30%;
  view-transition-name: card;
}

With Container Queries

/* Scroll animation respects container size */
.card-container {
  container-type: inline-size;
}

.card {
  animation: fade-in ease-out;
  animation-timeline: view();
}

@container (max-width: 400px) {
  .card {
    animation-range: entry 0% cover 50%;
  }
}

With CSS Custom Properties

/* Dynamic scroll animation with custom properties */
:root {
  --scroll-progress: 0;
}

@keyframes dynamic-color {
  from { 
    background-color: hsl(0, 70%, 50%);
    transform: scale(1);
  }
  to { 
    background-color: hsl(calc(var(--scroll-progress) * 360), 70%, 50%);
    transform: scale(calc(1 + var(--scroll-progress)));
  }
}

.element {
  animation: dynamic-color linear;
  animation-timeline: scroll();
}

Performance Best Practices

Use transform and opacity

/* Good: GPU-accelerated properties */
@keyframes efficient {
  to {
    transform: translateY(0);
    opacity: 1;
  }
}

/* Avoid: Layout-triggering properties */
@keyframes inefficient {
  to {
    width: 100%;
    height: 200px;
    margin: 20px;
  }
}

Limit Animation Scope

/* Animate only what's needed */
.animated-element {
  will-change: transform, opacity;
}

/* Avoid animating too many elements */
.scroll-container > * {
  animation: fade-in linear;
  animation-timeline: view();
}

Use view() with Margins

/* Start animation before element is visible */
.element {
  animation: slide-in ease-out;
  animation-timeline: view(100px);
  animation-range: entry 0% cover 30%;
}

Browser Support

Scroll-driven animations have growing browser support:

/* Feature detection */
@supports (animation-timeline: scroll()) {
  .element {
    animation-timeline: scroll();
  }
}
Browser Version Release Date
Chrome 115+ July 2023
Edge 115+ July 2023
Firefox 123+ March 2024
Safari 17.5+ May 2024

Progressive Enhancement

Design fallbacks for browsers without support:

/* Base styles - always visible */
.hero {
  opacity: 1;
  transform: translateY(0);
}

/* Enhanced styles for supporting browsers */
@supports (animation-timeline: scroll()) {
  .hero {
    opacity: 0;
    transform: translateY(50px);
  }
  
  .hero {
    animation: fade-in ease-out;
    animation-timeline: view();
    animation-range: entry 0% cover 40%;
  }
}

Common Patterns

Pattern 1: Progress Indicator

@keyframes grow-width {
  from { width: 0; }
  to { width: 100%; }
}

.progress {
  height: 3px;
  background: blue;
  
  animation: grow-width linear;
  animation-timeline: scroll();
}

Pattern 2: Fade In on Scroll

@keyframes fade-in-up {
  from {
    opacity: 0;
    transform: translateY(30px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.fade-on-scroll {
  animation: fade-in-up ease-out;
  animation-timeline: view();
  animation-range: entry 0% cover 40%;
}

Pattern 3: Scale on Scroll

@keyframes scale-on-scroll {
  from { transform: scale(1); }
  to { transform: scale(0.9); }
}

.shrinking-element {
  animation: scale-on-scroll linear;
  animation-timeline: scroll();
  animation-range: 0 500px;
}

Pattern 4: Rotation on Scroll

@keyframes rotate-on-scroll {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}

.rotating-element {
  animation: rotate-on-scroll linear;
  animation-timeline: scroll();
}

Debugging

Chrome DevTools

  1. Open DevTools (F12)
  2. Navigate to the “Animations” tab
  3. Scroll to see animations playing
  4. Click on animation to see timeline

Common Issues

/* Issue: Animation not triggering */
/* Solution: Check if animation-timeline is set correctly */

.element {
  animation-timeline: scroll(); /* or view() */
}

/* Issue: Animation range too short */
/* Solution: Adjust animation-range values */

.element {
  animation-range: entry 0% cover 40%;
}

/* Issue: Not working in scroll container */
/* Solution: Use scroll(self) or scroll(nearest) */

.element {
  animation-timeline: scroll(self);
}

External Resources

Summary

CSS Scroll-driven Animations enable powerful scroll-based effects using pure CSS:

  • animation-timeline: Controls what the animation is tied to (scroll or view)
  • animation-range: Controls when animation starts and ends
  • scroll(): Links to document or container scroll position
  • view(): Links to element’s visibility in viewport
  • Performance: Use transform and opacity for smooth animations
  • Browser Support: Chrome 115+, Edge 115+, Firefox 123+, Safari 17.5+

Start using scroll-driven animations to create engaging, performant scroll effects without JavaScript!

Comments