CSS Scroll-Driven Animations
Link CSS animations to scroll position using animation-timeline, creating progress bars, reveal effects, and parallax purely in CSS.
Overview
Key Points
- •animation-timeline: scroll() links animation progress to the nearest scroll ancestor
- •animation-timeline: view() links animation to the element entering/leaving the scroll viewport
- •No JavaScript needed; the browser drives timing from scroll position
- •animation-range lets you target entry, exit, contain, and cover phases of a view timeline
- •@scroll-timeline is the older API; prefer the newer scroll() and view() function syntax
- •Named scroll timelines can be created with scroll-timeline-name and referenced by other elements
- •Combine with animation-fill-mode: both to hold the end state
- •Supported in Chrome 115+ and Safari 18+
Examples
Reading progress bar at the top of the page
.reading-progress {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 4px;
background: #6366f1;
transform-origin: left;
animation: progress linear both;
animation-timeline: scroll(root);
}
@keyframes progress {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}scroll(root) uses the root document scroll container. The bar grows from 0 to full width as the user scrolls from top to bottom.
Fade and slide in as elements enter the viewport
.reveal {
opacity: 0;
transform: translateY(40px);
animation: reveal-item linear both;
animation-timeline: view();
animation-range: entry 0% entry 60%;
}
@keyframes reveal-item {
from {
opacity: 0;
transform: translateY(40px);
}
to {
opacity: 1;
transform: translateY(0);
}
}The animation plays from 0% to 100% of the element entering the viewport. Once it has entered 60% of the way, the animation is complete and held (fill-mode: both).
Sticky header shrink on scroll
.site-header {
position: sticky;
top: 0;
z-index: 100;
background: white;
animation: shrink-header linear both;
animation-timeline: scroll(root);
animation-range: 0px 200px; /* triggers in first 200px of scroll */
}
@keyframes shrink-header {
from {
padding: 24px 32px;
box-shadow: none;
}
to {
padding: 12px 32px;
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
}
}Named scroll timeline for sibling elements
/* Define a named timeline on the scroll container */
.scroll-section {
overflow-y: scroll;
height: 400px;
scroll-timeline-name: --section-scroll;
scroll-timeline-axis: block;
}
/* Reference the named timeline from a sibling */
.sibling-indicator {
animation: track-scroll linear both;
animation-timeline: --section-scroll;
}
@keyframes track-scroll {
from { background: #e0e7ff; }
to { background: #4338ca; }
}Named timelines starting with -- let elements outside the scroll ancestor animate based on a specific container's scroll.
Common Mistakes
- Forgetting animation-fill-mode: both causing the element to snap back to its starting state after the animation
- Using animation-duration: auto is required when using animation-timeline (you cannot combine a time-based duration with a scroll timeline)
- Expecting scroll-driven animations to work in Firefox (Chrome 115+ and Safari 18+ only as of 2024)
- Not testing with prefers-reduced-motion: reduce, scroll animations can cause discomfort
Note
Always wrap scroll-driven animations in a @media (prefers-reduced-motion: no-preference) block. Users who prefer reduced motion should not see scroll-driven parallax or entrance animations.
Exercises
Write CSS to style the HTML scaffold. The preview updates live — when it looks right, mark as done.
Completed: 0 / 2 exercises
Knowledge Check
Question 1 of 3