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
- Open DevTools (F12)
- Navigate to the “Animations” tab
- Scroll to see animations playing
- 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
- MDN: animation-timeline
- CSS Working Group: Scroll-driven Animations
- Chrome Dev: Scroll-driven animations
- Can I Use: Scroll-driven animations
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