animation-timeline, scroll(), view() and parallax without JS
Scroll animations, parallax effects and reveal transitions were built with JavaScript libraries for years. CSS Scroll-Driven Animations make that obsolete: animation-timeline, scroll() and view() enable performant, declarative scroll animations right in the stylesheet, running on the compositor thread with no layout thrashing.
Table of Contents
- 1. Why scroll animations belong in CSS
- 2. animation-timeline: the new centerpiece
- 3. scroll(): scroll progress as a timeline
- 4. view(): visibility as an animation trigger
- 5. ScrollTimeline and ViewTimeline in JS
- 6. Parallax effects without JavaScript
- 7. Reveal animations with view()
- 8. Scroll-Driven Animations compared
- 9. Performance, accessibility and browser support
- 10. Summary
- 11. FAQ
1. Why scroll animations belong in CSS
Scroll-based animations have long been considered the domain of JavaScript libraries on the web: GSAP ScrollTrigger, Intersection Observer, requestAnimationFrame loops, all JavaScript, all running on the main thread. The problem here is not just the dependency, but above all the performance: JavaScript-driven animations that read or set layout properties can trigger reflow and block the main thread. The result is janky animations that stand out in performance audits. CSS Scroll-Driven Animations operate directly on the compositor thread and sidestep this problem from the ground up.
The specification was introduced as part of CSS Animations Level 2 and is closely tied to the Web Animations API. The core idea: a CSS animation gets a scroll timeline as its animation-timeline instead of the default time-based timeline. The animation then progresses not with time, but with scroll progress. CSS Scroll-Driven Animations are therefore not a new animation type, but an extension of the existing animation model with scroll-based timelines.
2. animation-timeline: the new centerpiece
The CSS property animation-timeline is the central new concept in CSS Scroll-Driven Animations. It replaces or extends the default behavior in which animations progress over time. With animation-timeline: scroll() or animation-timeline: view(), the timeline is bound to a scroll container or to the visibility of an element. The property accepts three kinds of values: the scroll() function, the view() function, or the name of a named timeline defined with scroll-timeline-name or view-timeline-name.
Important for a correct understanding: animation-timeline does not change what is animated, that is still determined by @keyframes. It changes how the animation progresses over time. A progress value of 0% corresponds to the start of scrolling (or entering the viewport), 100% to the end. All keyframe positions relate to this progress value. That suddenly makes it possible to achieve entirely new effects with familiar CSS keyframe techniques that used to require JavaScript.
/* Reading progress bar: scroll-driven, zero JavaScript */
@keyframes progress-grow {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
.reading-progress {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 4px;
background: linear-gradient(90deg, #7c3aed, #c4b5fd);
transform-origin: left center;
transform: scaleX(0);
/* Link animation to the document scroll position */
animation: progress-grow linear;
animation-timeline: scroll(root block);
/* root = <html> scroll container, block = vertical axis */
}
/* Fade-in element as page scrolls */
@keyframes fade-in-up {
from {
opacity: 0;
translate: 0 2rem;
}
to {
opacity: 1;
translate: 0 0;
}
}
.hero-title {
animation: fade-in-up linear;
animation-timeline: scroll(root);
animation-range: 0% 20%;
/* Only plays during the first 20% of page scroll */
}
3. scroll(): scroll progress as a timeline
The scroll() function creates an anonymous CSS Scroll-Driven Animation timeline that is coupled to the scroll progress of a specific scroll container. It takes two optional parameters: the scroll container and the scroll axis. The container parameter can be nearest (the nearest scrollable ancestor), root (the document itself) or self (the element itself). The axis can be block (typically vertical), inline (horizontal) or x/y.
The progress value of the scroll() timeline runs from 0%, when the scroll container is at the very top, to 100%, when it is at the very bottom. The animation responds linearly to scroll progress, unless animation-timing-function transforms the progress. Combined with animation-range, you can precisely control in which scroll range the animation is active. That enables effects that only run in a specific section of the page, a core trait of well-designed CSS Scroll-Driven Animations.
/* Horizontal gallery scrolled with scroll() on inline axis */
.gallery-track {
display: flex;
overflow-x: scroll;
scroll-snap-type: x mandatory;
/* Named scroll timeline for the track */
scroll-timeline-name: --gallery;
scroll-timeline-axis: inline;
}
/* Counter shows how far through the gallery we are */
.gallery-counter {
animation: count-progress linear;
animation-timeline: --gallery;
}
@keyframes count-progress {
from { --progress: 0; }
to { --progress: 100; }
}
/* Sticky header shrinks as user scrolls down */
@keyframes shrink-header {
from {
padding-block: 1.5rem;
font-size: 1.125rem;
background: rgba(255, 255, 255, 0);
}
to {
padding-block: 0.75rem;
font-size: 0.875rem;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(12px);
box-shadow: 0 1px 8px rgba(0,0,0,0.1);
}
}
.site-header {
position: sticky;
top: 0;
animation: shrink-header linear both;
animation-timeline: scroll(root block);
animation-range: 0 200px; /* Active during first 200px of scroll */
}
4. view(): visibility as an animation trigger
The view() function is the second major pillar of CSS Scroll-Driven Animations. While scroll() is coupled to the overall progress of a scroll container, view() binds the animation to the visibility of the animated element itself within the viewport. The timeline starts when the element enters the viewport and ends when it leaves it. The 0% point marks the moment of entry, the 100% point marks complete exit.
The animation-range property lets you control exactly in which visibility range the animation is active. Values can be absolute lengths or percentages of the scrollport size. Particularly useful are the named range values: entry (entering the viewport), exit (leaving the viewport), contain (element fully visible) and cover (element fully overlaps the viewport). These semantic ranges make it easy to build reveal animations that play exactly when the element comes into view, a common design pattern in modern CSS Scroll-Driven Animations implementations.
5. ScrollTimeline and ViewTimeline in JavaScript
The JavaScript counterparts to scroll() and view() are ScrollTimeline and ViewTimeline from the Web Animations API. They enable the same effects programmatically, useful when animations are created dynamically or bound to DOM elements that are not yet known at stylesheet time. new ScrollTimeline({ source: scrollContainer, axis: 'block' }) creates a timeline bound to the scroll progress of the given container. This timeline can then be passed to an Element.animate() animation.
ViewTimeline takes subject (the element being observed) and axis as parameters. Combining both APIs allows for hybrid implementations: the animation logic stays in CSS (@keyframes and animation-timeline with named timelines), while JavaScript only handles the timeline assignment. That is useful for frameworks and component libraries that need to assign CSS Scroll-Driven Animations dynamically without knowing the CSS in advance.
/* Named view timeline: reusable across multiple elements */
.reveal-item {
/* Each element is its own subject */
view-timeline-name: --reveal;
view-timeline-axis: block;
animation: slide-in linear both;
animation-timeline: --reveal;
animation-range: entry 0% entry 50%;
/* Plays while element enters the viewport (0% to 50% of entry) */
}
@keyframes slide-in {
from {
opacity: 0;
translate: 0 3rem;
filter: blur(4px);
}
to {
opacity: 1;
translate: 0 0;
filter: blur(0);
}
}
/* Stagger delay using animation-range offset */
.reveal-item:nth-child(2) { animation-range: entry 5% entry 55%; }
.reveal-item:nth-child(3) { animation-range: entry 10% entry 60%; }
.reveal-item:nth-child(4) { animation-range: entry 15% entry 65%; }
/* Exit animation: element fades out as it leaves viewport */
.fade-exit {
view-timeline-name: --exit;
animation: fade-out linear both;
animation-timeline: --exit;
animation-range: exit 0% exit 100%;
}
@keyframes fade-out {
to { opacity: 0; translate: 0 -2rem; }
}
6. Parallax effects without JavaScript
Parallax scrolling, where different layers scroll faster or slower than each other, used to be one of the most involved scroll animations to implement with JavaScript. With CSS Scroll-Driven Animations it is surprisingly simple. The basic principle: an element gets a scroll() timeline and moves via transform: translateY() at a different pace than normal scrolling. Because the animation progress is linearly tied to scroll progress, different keyframe values automatically produce the parallax effect.
For performance, one thing is decisive: only use CSS properties that the browser can animate on the compositor thread, above all transform and opacity. Animating top, left, margin or height forces the browser into reflow and loses the performance benefit of CSS Scroll-Driven Animations. The same applies to animations driven by a JavaScript ScrollTimeline. The CSS property will-change: transform signals the browser to keep the element on a separate compositor layer, further accelerating the animation.
7. Reveal animations with view()
Reveal animations, elements that fade or slide into view as the user scrolls, are one of the most common use cases for CSS Scroll-Driven Animations. Previously this required the Intersection Observer API, JavaScript callbacks and toggling CSS classes. With view() and animation-range: entry, the whole thing can be expressed purely in CSS. Any element carrying the .reveal class animates itself automatically as it enters the viewport, with no JavaScript, no ResizeObserver, no MutationObserver.
For accessible implementations, @media (prefers-reduced-motion: reduce) is essential. Users who have enabled reduced motion do not expect animated transitions. The recommendation: disable reveal animations entirely or replace them with simple opacity transitions when prefers-reduced-motion: reduce is set. That is both accessibility friendly and technically simple: a single media query at the end of the stylesheet is enough to disable all CSS Scroll-Driven Animations.
8. Scroll-Driven Animations compared
The choice between native CSS Scroll-Driven Animations and JavaScript solutions depends on complexity, browser support requirements and the nature of the animation. For standard reveal animations and progress bars, native CSS solutions are clearly superior. For complex sequences with dependencies between multiple elements, JavaScript remains the more powerful tool.
| Use case | CSS scroll()/view() | Intersection Observer | GSAP ScrollTrigger |
|---|---|---|---|
| Reveal on entry | Ideal, view() + entry | Good, with a JS class | Possible, with overhead |
| Progress bar | Perfect, scroll(root) | Not suited | Possible |
| Parallax layers | Good, transform + scroll() | Not suited | Good, more control |
| Complex sequences | Limited | Limited | Ideal |
| Performance | Compositor thread | Main thread (JS) | Main thread (JS) |
An often overlooked advantage of CSS Scroll-Driven Animations over Intersection Observer: the animations are reversible. When an element scrolls backward, the animation reverses too, with no extra logic required. Intersection Observer only fires on state changes and requires explicit logic for reverse animations. That makes CSS scroll animations especially elegant for bidirectional effects such as sticky header transformations or parallax backgrounds.
9. Performance, accessibility and browser support
CSS Scroll-Driven Animations run on the compositor thread in Chrome and Safari when only compositor-friendly properties are animated: transform, opacity, filter and clip-path. All other properties, including colors, dimensions and spacing, force main thread involvement. That is not a disadvantage compared to JavaScript, but it means the performance benefit of CSS Scroll-Driven Animations can only be fully realized with disciplined property choices.
Browser support: Chrome 115+, Safari 18+ and Firefox 110+ support the core features of CSS Scroll-Driven Animations. Support for individual features such as animation-range with named range values varies. Progressive enhancement: outside of @supports (animation-timeline: scroll()) blocks, elements remain visible in their static state, which is sensible, because content that never blinks is better than content that stays invisible forever because the animation never ran.
/* Progressive enhancement: full accessibility support */
/* Default: always visible, no animation */
.reveal-section {
opacity: 1;
translate: 0 0;
}
/* Enhancement: animate only when supported */
@supports (animation-timeline: view()) {
.reveal-section {
opacity: 0;
translate: 0 2.5rem;
animation: reveal-section linear both;
animation-timeline: view();
animation-range: entry 0% entry 60%;
}
}
@keyframes reveal-section {
to {
opacity: 1;
translate: 0 0;
}
}
/* Respect user preference: disable all scroll animations */
@media (prefers-reduced-motion: reduce) {
.reveal-section,
.parallax-layer,
.reading-progress {
animation: none !important;
opacity: 1 !important;
translate: 0 0 !important;
transform: none !important;
}
}
10. Summary
CSS Scroll-Driven Animations are one of the most significant extensions of the CSS animation model in years. With animation-timeline: scroll(), an animation is bound to the scroll progress of a container; with animation-timeline: view(), it is bound to the visibility of the element itself. Named timelines (scroll-timeline-name, view-timeline-name) let you link animations to scroll containers across DOM levels. animation-range allows precise control over which scroll or visibility range the animation is active in.
The practical payoff: progress bars, reveal animations, parallax effects, sticky header transformations and fade-in transitions can be expressed entirely in CSS, without JavaScript, without Intersection Observer, without requestAnimationFrame. The performance benefits of compositor thread execution come on top, as long as only compositor-friendly properties are animated. For accessible implementations, @media (prefers-reduced-motion: reduce) is mandatory and should never be missing from a CSS Scroll-Driven Animations project.
CSS Scroll-Driven Animations: the essentials at a glance
scroll()
animation-timeline: scroll(root block) binds the animation to the document's scroll progress. 0% = top, 100% = bottom.
view()
animation-timeline: view() runs the animation as the element appears in the viewport. animation-range: entry controls the range.
Performance
Only animate transform, opacity, filter for the compositor thread. Layout properties force main thread involvement.
Accessibility
@media (prefers-reduced-motion: reduce) disables all scroll animations. Ensure baseline visibility without @supports.
Mironsoft
Modern CSS, animations and Hyva frontend development
Scroll animations without JavaScript overhead?
We implement CSS Scroll-Driven Animations for Hyva and Magento projects: reveal effects, parallax backgrounds and animated hero sections, performant, accessible and without a single line of JavaScript.
Animation audit
Performance analysis of existing JS animations and a migration strategy to CSS-native solutions
Reveal systems
Scroll-driven reveal animations for product listings, content pages and landing pages
Parallax & hero
Compositor-optimized parallax effects and animated hero sections for strong Core Web Vitals