CSS Scroll-Driven Animations: Zero-JavaScript Scroll Effects and Reading Bars
Master CSS scroll-driven animations with animation-timeline: scroll() and view(). Build reading progress bars and reveal-on-scroll effects in pure CSS.

Historically, creating scroll-linked animationsβsuch as a top reading progress bar, sticky header shrink effects, or elements that fade and scale in as they enter the viewportβrequired attaching JavaScript window.addEventListener('scroll', ...) listeners or running requestAnimationFrame loops.
These JavaScript scroll listeners frequently caused scroll jank and high CPU consumption because they ran on the browser main thread.
CSS Scroll-Driven Animations connect standard @keyframes animations directly to the scroll progress of a container or the viewport, executed directly on the Compositor Thread at 120fps with zero JavaScript.
1. scroll() vs. view() Timelines
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 1. animation-timeline: scroll() β
β Driven by the scroll position of the entire page or β
β scroll container (0% at top, 100% at bottom). β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β 2. animation-timeline: view() β
β Driven by an individual element's visibility as it β
β enters, crosses, and exits the viewport. β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
2. Recipe 1: Pure CSS Reading Progress Bar
Create a top reading progress bar with 3 lines of CSS:
<div class="reading-progress-bar"></div>
.reading-progress-bar {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 4px;
background: linear-gradient(90deg, #0284c7, #38bdf8);
transform-origin: 0 50%;
/* Link animation to page scroll timeline */
animation: grow-progress auto linear;
animation-timeline: scroll(root block);
}
@keyframes grow-progress {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
3. Recipe 2: Reveal Elements on Scroll with view()
Animate cards as they scroll into view:
.reveal-card {
animation: fade-slide-in ease-out both;
animation-timeline: view();
animation-range: entry 20% cover 40%;
}
@keyframes fade-slide-in {
from {
opacity: 0;
transform: translateY(40px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
animation-range: entry 20% cover 40%: Starts the animation when the element is 20% into the viewport and completes by 40% coverage.
4. Respecting prefers-reduced-motion
Always disable scroll-driven movement for users with vestibular sensitivities:
@media (prefers-reduced-motion: reduce) {
.reading-progress-bar,
.reveal-card {
animation: none !important;
}
}
Next Steps
- Continue to the next guide: CSS Popover API & Dialog Element.
