Native Scroll Timelines Are Here: Does Every Scroll Effect Still Need JavaScript?
CSS scroll-driven animations can replace JavaScript for many scroll effects, but not all. See where native timelines fit, where JS still earns its place, and how to choose.


Hitesh Bhardwaj
Sr. Developer
Category: Engineering
A reading-progress bar has accumulated a surprising amount of JavaScript over the years.
Listen for scroll. Read the offset. Calculate a percentage. Write a transform. Throttle the handler or move the work into requestAnimationFrame. Remove the listener when the component unmounts.
CSS scroll-driven animations remove that machinery from a useful class of effects. The browser can use scroll or view progress as the timeline for CSS keyframes, so progress indicators, restrained reveals and simple parallax treatments no longer need application code translating pixels into animation progress.
The short answer is still not “JavaScript is dead.”
Native CSS is a strong first implementation to test when scroll progress maps directly to presentation. JavaScript still earns its place when the interaction depends on direction, velocity, callbacks, application state, complex sequencing, canvas or WebGL rendering, or compatibility requirements that need scripted behavior.
The useful question is no longer “CSS or JavaScript?” It is: what does this particular interaction actually need?
What CSS scroll-driven animations actually changed
Traditional CSS animations run against time. A 2s animation progresses because two seconds pass.
Scroll-driven animations replace that clock with scroll progress. animation-timeline can connect CSS keyframes to either a scroll progress timeline or a view progress timeline. scroll() tracks a scroll container; view() tracks an element as it moves through its scrollport.
A reading-progress indicator can therefore be almost suspiciously small:
@keyframes reading-progress {
from {
transform: scaleX(0);
}
to {
transform: scaleX(1);
}
}
.reading-progress {
position: fixed;
inset: 0 0 auto;
height: 3px;
transform-origin: left;
animation: reading-progress 1ms linear;
animation-timeline: scroll(root block);
}
There is no scroll listener calculating scrollY / scrollHeight. The browser already owns the scroll position; CSS is simply using it as the timeline.
The slightly odd 1ms duration is intentional. Scroll progress still controls the effective animation progress, but MDN currently recommends a non-zero duration because Firefox requires one for the animation to apply correctly.
There is another copy-paste trap: the animation shorthand resets animation-timeline to auto, so the timeline declaration should come after the shorthand.
A view-based reveal uses the same model:
@keyframes reveal {
from {
opacity: 0;
transform: translateY(2rem);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.reveal {
animation: reveal 1ms linear both;
animation-timeline: view();
animation-range: entry 10% cover 35%;
}
Here, document progress is irrelevant. The animation follows the element's journey through the viewport.
That covers a useful slice of creative frontend work: progress bars, image scaling, restrained parallax and section treatments where scroll progress maps directly to CSS-animatable presentation. A progress bar should not require a small event-listener bureaucracy.
Scroll-driven and scroll-triggered are different problems
Suppose a card enters the viewport and then fades in over 400 milliseconds. Scroll determines when the animation starts. Time determines how the animation progresses after that.
Now take an image that moves from translateY(-10%) to translateY(10%) in exact proportion to its journey through the viewport. Scroll determines the animation's progress continuously. Those are different control models.
A scroll-driven animation handles the second case: progress is tied to scrolling. A scroll-triggered animation starts, pauses, resets or otherwise controls a normal time-based animation when a scroll boundary is crossed.
Historically, that trigger case often meant IntersectionObserver. As of Chrome 146, Chromium also supports declarative CSS scroll-triggered animations using timeline triggers and animation triggers, including timeline-trigger and animation-trigger. Chrome explicitly positions the feature as a CSS alternative for many interactions previously built with viewport-detection JavaScript.
That does not make IntersectionObserver obsolete. Current compatibility data shows animation-trigger support in Chromium-family browsers from version 146, while Firefox and Safari do not yet support it. For a cross-browser one-shot reveal today, an observer may still be the simpler production choice.
So there are now three distinct questions:
| Job | Natural starting point |
|---|---|
| Progress follows scrolling continuously | scroll() / view() timeline |
| Crossing a scroll boundary starts a time-based animation | CSS scroll trigger where support fits; otherwise IntersectionObserver |
| Scroll changes application state or executes richer logic | JavaScript |
Treating all three as “scroll animation” is how implementation decisions get muddy.
Where native CSS should be the first implementation to test
The strongest native candidates share one trait: scroll position is already the state.
A long-form article progress line needs one value: how far through the document the reader has travelled. A product image that scales from 1.08 to 1 while entering the viewport has a similarly direct relationship. View progress maps to scale. The same applies to restrained parallax or an opacity treatment whose only job is to move between visual states across a defined range.
| Interaction | Native CSS fit | Why | Typical fallback |
|---|---|---|---|
| Reading progress indicator | Strong | Scroll progress is the entire input | Hide the decorative indicator |
| View-linked reveal | Strong | View progress maps directly to keyframes | Render content fully visible |
| Modest image parallax | Strong | One continuous value drives a transform | Static image |
| Scale/opacity treatment | Strong | Declarative range expresses the behavior | Final readable state |
| Scroll direction-sensitive header | Weak | Behavior depends on direction and retained state | JavaScript logic |
| Velocity-reactive motion | Weak | Current speed affects output | JavaScript/library logic |
| Multi-scene pinned narrative | Depends | Sequencing and pinning can exceed simple timelines | Simplified vertical flow |
| WebGL scene control | Weak | Scroll must update rendering state | Static/poster or simplified scene |
The threshold is not visual ambition. A polished effect can still be simple under the hood.
The better test is:
When progress moves from A to B, can the effect be described as animating presentation values from X to Y?
When the answer is yes, native CSS deserves the first attempt.
Where JavaScript still earns its place
Scroll position is not always enough information.
Consider a navigation bar that disappears while the user scrolls down and returns when they reverse direction. The important value is not only where the page is. The interaction needs to know which way the user is moving.
A kinetic card stack might respond more strongly to a fast flick than a slow scroll. Now velocity matters.
A product story might change React state when the reader enters chapter three, update an active navigation item, swap media sources and record an analytics event. Scroll is coordinating application behavior, not merely changing presentation.
Those stateful requirements still belong naturally in JavaScript.
Mature animation tooling also remains useful when an interaction needs scrubbed multi-step timelines, snapping, callbacks, velocity, direction or advanced pinning. GSAP ScrollTrigger currently exposes those controls directly, including getVelocity(), direction, snapping, callbacks and pinned ranges.
A pinned feature sequence is a good example. One panel may stay in place while child timelines enter, overlap and exit at deliberate points. Labels may define chapters. Snapping may pull scrolling toward stable states. Resizing may change calculated ranges.
You can construct parts of that system with CSS, sticky positioning and native timelines. Whether that is the better implementation depends on whether the resulting composition is simpler than the orchestration it replaces.
Native APIs are building blocks, not a purity test.
Canvas and WebGL make the boundary clearer
CSS can animate the position, opacity or transform of a <canvas> element. It does not replace the rendering system inside that canvas.
If scrolling moves a Three.js camera through a scene, changes shader uniforms, morphs geometry or controls postprocessing, application code still has to translate interaction state into rendering state.
In React Three Fiber, that may involve refs, hooks, animation values or a render loop. Production quality also means cleaning up listeners and timelines, pausing expensive work when a scene is hidden or offscreen, and reducing rendering cost where mobile hardware does not justify the desktop treatment.
Meaningful copy, links and calls to action should remain in normal HTML rather than becoming dependent on a decorative canvas. A shader does not become CSS because the page has learned a new timeline.
Native does not automatically mean faster in every case
Scroll-driven CSS has a meaningful architectural advantage: the browser owns both scrolling and the animation timeline.
That can avoid a synchronization problem where main-thread JavaScript repeatedly reads scroll state and tries to keep visual updates aligned with browser scrolling. It is still worth avoiding the usual performance slogans.
A native timeline does not turn every animated property into a cheap one. Layout-heavy properties can still create more work than transforms or opacity. Large paint areas remain large paint areas. Several complex effects can still compete for rendering resources. The performance path also depends on the browser implementation. Safari 26.4 added threaded scroll-driven animations, allowing eligible scroll-driven animations to run on the compositor thread separately from the main thread. That is a concrete implementation improvement. It is not proof that every native scroll animation in every browser is automatically off-main-thread.
Profile the page you intend to ship. Test a production build, inspect rendering and main-thread activity in browser DevTools, and check real devices before turning “native” into a performance claim.
Browser support still changes the production decision
Native scroll timelines are useful, but they are not yet a universal baseline.
MDN currently marks animation-timeline as Limited availability. Firefox 155 contains scroll-driven animations as an experimental feature, enabled by default in Nightly but disabled by default in Release, Beta and Developer Edition behind layout.css.scroll-driven-animations.enabled. MDN: animation-timeline browser status Mozilla: Firefox 155 release notes
That makes progressive enhancement more important than API enthusiasm.
Start from a useful static state:
.reveal {
opacity: 1;
transform: none;
}
@supports (animation-timeline: view()) {
.reveal {
animation: reveal 1ms linear both;
animation-timeline: view();
animation-range: entry 10% cover 35%;
}
}
@media (prefers-reduced-motion: reduce) {
.reveal {
animation: none;
opacity: 1;
transform: none;
}
}
A browser without timeline support receives readable content instead of copy stuck at opacity: 0. That is often enough. A decorative reveal does not necessarily deserve a JavaScript polyfill merely to preserve motion everywhere.
Functional behavior is different. If scroll state drives navigation, content selection or another essential interaction, unsupported browsers need a real alternative path rather than a missing effect. The animation's job decides how expensive its fallback needs to be.
Reduced motion is only part of accessibility
prefers-reduced-motion matters, but it is not the entire accessibility strategy.
A robust scroll interaction should still make sense when its motion disappears.
Keep content in a logical DOM order. Keyboard users should be able to reach links, controls and sections without depending on a pinned sequence. Required information should not exist only inside a transform, canvas, hover state or animated transition.
For scroll storytelling, the static document should still communicate the story. That may mean collapsing a pinned sequence into a normal vertical flow on smaller screens or reduced-motion configurations. It may mean showing the final readable state immediately instead of replaying a reveal. It may mean keeping semantic HTML outside a decorative WebGL layer.
Motion can control presentation. It should not become the only route to meaning.
React does not force you into either camp
A React project does not make a CSS effect less native.
If a component's scroll behavior is entirely presentational, React can render the markup and CSS can own the timeline. In Next.js, that component can remain server-rendered if it otherwise has no client-side behavior. Once the interaction needs React state, DOM measurement, browser APIs, animation hooks or a JavaScript engine, the boundary changes.
| Requirement | Likely implementation |
|---|---|
| CSS-only scroll() or view() effect | Server-rendered markup + CSS can be enough |
| Cross-browser viewport trigger | Client-side observer or another fallback |
| Motion useScroll values | Client component |
| GSAP ScrollTrigger | Client component with lifecycle cleanup |
| Canvas or WebGL scene | Client-side rendering path in most cases |
| Decorative unsupported-browser fallback | Static server-rendered content + progressive enhancement |
Libraries can also sit between pure CSS and fully scripted control.
Motion's current React documentation says its scroll-linked animations use the browser's native ScrollTimeline where possible and fall back to JavaScript when necessary. Its useScroll API exposes scroll progress for composition inside React.
So “library” and “native” are no longer opposites in every implementation.
The architectural question is whether the abstraction is buying something: compatibility, composition, state integration, sequencing or a consistent API across several effects. If it is wrapping three lines of CSS without adding any of those things, the wrapper has some explaining to do.
A four-question test for your next scroll effect
Before adding another scroll dependency, run the interaction through four questions.
1. Can CSS express the output?
If the result is a direct animation of transform, opacity, clip-path or another CSS-animatable presentation value, test a native timeline first.
2. Does the behavior need more than progress?
Direction, velocity, callbacks, application state, analytics, dynamic measurements or other side effects are signs that JavaScript has a real job.
3. What happens when the timeline is unavailable?
If the page can fall back to a static readable state, progressive enhancement keeps native CSS attractive. If the scroll interaction controls essential functionality, design an explicit fallback.
4. Does a library reduce the actual complexity?
A mature animation library is not automatically excessive because it contains more capability than one CSS property. If the page already coordinates multiple timelines, pins, snap points, responsive recalculation and state changes, the abstraction may remove more complexity than it introduces.
| If your interaction… | Start here |
|---|---|
| Maps scroll/view progress directly to CSS presentation | Native scroll() / view() |
| Starts a time-based animation at a scroll boundary and Chromium-only support is acceptable | CSS scroll-triggered animation |
| Needs that trigger across current major browsers | IntersectionObserver or a library fallback |
| Needs state, direction, velocity or callbacks | JavaScript |
| Needs coordinated pinning, snapping and timelines | Animation engine such as GSAP |
| Needs scroll to manipulate canvas/WebGL state | JavaScript/rendering layer |
Choose the tool that leaves the least unnecessary orchestration after the real requirements are satisfied.
A small demo is enough to test the model
Before introducing a library, build one deliberately boring experiment:
- Add a fixed progress bar using scroll(root block).
- Add a content card using view().
- Turn JavaScript off.
- Confirm the page still reads correctly when the animation is unsupported.
- Enable reduced motion and confirm nothing essential disappears.
- Profile the production build before deciding whether the effect needs more machinery.
That six-step test exposes most of the architecture quickly. If the interaction already works, reads correctly and degrades cleanly, adding an engine should solve a problem you can name.
If it does not, you have found the reason JavaScript belongs there.
What this means for Vault scroll effects
Treating every scroll effect as though it needs the same engine would now be especially poor architecture.
Hyperiux Vault uses an effect-level dependency model. Some patterns rely on React and CSS. Others use GSAP, Motion, Three.js or additional rendering tools when their implementation requires those capabilities. The dependencies stay visible with the effect rather than being assumed for an entire category.
That distinction matters more as browsers absorb a larger share of straightforward scroll-linked presentation.
Take Vault's Parallax Image effect. Its current implementation uses GSAP for scrubbed image movement and optional scaling. Its effect page also documents mobile, reduced-motion and performance considerations rather than treating the demo as a universal implementation guarantee.
That does not mean every parallax effect needs GSAP.
A quiet image offset that only maps view progress onto translateY may now be an excellent native CSS candidate. A composed sequence with multiple ranges, state changes, responsive recalculation or timeline coordination has different requirements.
Editable source makes that choice visible.
The engine, timing, breakpoints, fallback and reduced-motion strategy can change with the page instead of becoming an installation-time assumption.
The rule is simpler than “CSS good, JavaScript bad”
Native scroll timelines have removed a lot of JavaScript that never needed to be interesting.
A progress indicator can stay close to the styles it changes. A view-linked effect can use the viewport as its timeline. Chrome's newer scroll-trigger API can even remove JavaScript from some threshold-based time animations when its browser support is sufficient.
JavaScript remains useful when the interaction becomes stateful, orchestrated or connected to a rendering system outside CSS. GSAP, Motion and custom logic still solve problems that scroll timelines were not designed to absorb.
So start one level lower than you used to.
Use the browser when progress maps directly to presentation. Escalate when the interaction gives you a concrete reason: state, direction, velocity, callbacks, choreography, rendering or compatibility.




