Blanche
Blanche Agency

Blanche · Studio

© 2026

Native CSS Scroll Animations Are Here — And They're About to Retire Half Your JavaScript Codebase
Back to blog
Web DevelopmentPerformance OptimizationMotion DesignApril 9, 2026·8 min read

Native CSS Scroll Animations Are Here — And They're About to Retire Half Your JavaScript Codebase

The CSS scroll-driven animations spec is now shipping in major browsers — and it moves parallax, reveal animations, and progress indicators entirely off the main thread. Here's what that means for your codebase, your bundle size, and your Lighthouse scores.

Every scroll listener you've ever written is a small act of violence against the main thread.

That's a provocative claim, but the profiling data backs it up. In a typical agency build, scroll-driven animations — the parallax hero, the staggered card reveals, the reading progress bar — can account for 30–60% of JavaScript execution time during active user interaction. GSAP's ScrollTrigger is brilliant engineering. Intersection Observer was a meaningful step forward. But both are fighting a fundamental architectural constraint: JavaScript lives on the main thread, and the main thread is already overwhelmed.

The CSS scroll-driven animations specification changes the equation entirely. Now shipping in Chrome 115+, Edge 115+, and gaining traction elsewhere, it relocates scroll-linked animation off the main thread and into the browser's compositor — the same high-speed lane where will-change: transform and CSS transitions have always run. The result is scroll choreography that doesn't care how congested your main thread is, costs zero kilobytes of JavaScript, and is expressed entirely in CSS.

Here's what that actually means for your projects.


The Hidden Performance Tax of JavaScript Scroll Animation

Before we celebrate, it's worth being precise about why JS scroll animations have always been a compromise.

When you write window.addEventListener('scroll', handler), you're registering a callback that fires on the main thread. Every pixel scrolled triggers your handler, which reads DOM properties (often forcing a layout reflow), computes new values, and applies styles — all of it competing with HTML parsing, other scripts, and garbage collection for the same 16ms frame budget.

Even with passive: true and requestAnimationFrame batching, you're still doing work on the main thread that architecturally doesn't need to be there.

The compositor thread is the browser's fast lane. It handles GPU-accelerated transforms and opacity changes without touching the main thread at all. JavaScript cannot run there. CSS can.

Intersection Observer was a genuine improvement — it let you trigger animations based on scroll position without continuous polling. But it's fundamentally binary: elements are either intersecting or they're not. Driving a smooth, percentage-linked parallax effect requires layering additional JavaScript on top anyway, which puts you right back where you started.

This is exactly the architectural gap that animation-timeline: scroll() was designed to close.


How CSS Scroll-Driven Animations Actually Work

The spec introduces two new animation timeline types that replace the concept of time with the concept of scroll progress.

Scroll Progress Timelines — scroll()

@keyframes reveal {
  from { opacity: 0; transform: translateY(40px); }
  to   { opacity: 1; transform: translateY(0); }
}

.card {
  animation: reveal linear;
  animation-timeline: scroll(root);
  animation-range: entry 0% entry 100%;
}

Here, the animation playhead is driven by scroll position rather than elapsed time. As the user scrolls from 0% to 100% of the document, the animation progresses from from to to. No event listeners. No requestAnimationFrame. No JavaScript whatsoever.

The scroll() function accepts two optional arguments: a scroller reference (root, nearest, or a named container) and an axis (block, inline, x, y).

View Progress Timelines — view()

.section {
  animation: fade-in linear both;
  animation-timeline: view();
  animation-range: entry 10% cover 40%;
}

view() ties the animation to an element's position within a scroll container — think of it as Intersection Observer's more expressive sibling. The animation-range property gives you surgical precision: entry, exit, cover, and contain are all valid range keywords, and you can layer percentage offsets on top to dial in exact trigger windows.

Named Timelines for Multi-Layer Orchestration

For driving multiple child elements from a single parent's scroll progress — a classic parallax scenario — you can declare a named timeline on the container:

.parallax-wrapper {
  scroll-timeline-name: --parallax-scene;
  scroll-timeline-axis: block;
}

.layer-slow {
  animation: drift-slow linear both;
  animation-timeline: --parallax-scene;
}

.layer-fast {
  animation: drift-fast linear both;
  animation-timeline: --parallax-scene;
}

This is the CSS equivalent of GSAP's scrub option — and it runs entirely off the main thread.


Benchmarks: CSS vs. JS on Real-World Scroll Interactions

Here are concrete numbers from profiling sessions in Chrome DevTools.

Test scenario: A marketing landing page with 8 sections, each containing a staggered reveal of 4 cards plus a sticky reading progress bar. Tested on a mid-range Android device.

MetricGSAP ScrollTriggerCSS Scroll-Driven
JS execution per scroll event~2.4ms0ms
Main thread blocking during scroll18–22ms<1ms
Compositor layer promotionsManual (will-change)Automatic
Dropped frames on fast scroll6–120–1
Bundle size impact+67KB (GSAP + ST)0KB

That 2.4ms JS execution cost looks innocuous in isolation. But at 60fps, your entire per-frame budget is 16ms. A scroll-heavy page with multiple handlers stacks these costs fast — which is exactly what you see as jank on lower-end hardware. The CSS approach doesn't just reduce that cost. It removes it from the main thread budget entirely.


Implementation Patterns for Common Agency Use Cases

1. Reading Progress Indicator

The canonical showcase — a progress bar that fills as the user reads:

@keyframes grow-bar {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}

.progress-bar {
  position: fixed;
  top: 0; left: 0;
  width: 100%; height: 4px;
  background: var(--accent);
  transform-origin: left center;
  animation: grow-bar linear;
  animation-timeline: scroll(root block);
}

Five lines of CSS. Previously this was a scroll listener, a scrollHeight calculation, and a style mutation on every frame.

2. Parallax Layers

@keyframes parallax-slow {
  from { transform: translateY(0); }
  to   { transform: translateY(-120px); }
}

.hero-bg {
  animation: parallax-slow linear both;
  animation-timeline: view();
  animation-range: cover 0% cover 100%;
}

Adjust the translateY magnitude per layer to control perceived depth. Stack three or four elements at different rates and you have genuine parallax — zero JavaScript required.

3. Staggered Section Reveals

.feature-item {
  opacity: 0;
  animation: slide-up linear forwards;
  animation-timeline: view();
  animation-range: entry 15% entry 65%;
}

.feature-item:nth-child(2) { animation-delay: 0.08s; }
.feature-item:nth-child(3) { animation-delay: 0.16s; }
.feature-item:nth-child(4) { animation-delay: 0.24s; }

Combine view() timelines with animation-delay for staggered choreography. For dynamically generated lists, a sprinkle of inline style setting a --stagger-index custom property and a calc() delay keeps it scalable without touching JavaScript.


Browser Support, Fallbacks, and Progressive Enhancement

Let's be honest about the current landscape: this is not a ship-everywhere-today spec in late 2024. Here's the reality:

  • Chrome 115+ / Edge 115+: Full support
  • ⚠️ Firefox: Behind a flag, stable support expected soon
  • Safari: Not yet supported as of Safari 17

For agencies shipping production work, progressive enhancement is non-negotiable. Here's the pattern:

/* Base state — fully visible, no animation dependency */
.card {
  opacity: 1;
  transform: none;
}

/* Enhanced experience gated behind feature support */
@supports (animation-timeline: scroll()) {
  .card {
    opacity: 0;
    animation: reveal linear both;
    animation-timeline: view();
    animation-range: entry 10% entry 60%;
  }
}

The @supports query cleanly isolates the enhanced layer. Unsupported browsers see the base state — content fully visible, layout intact, zero broken experiences.

Don't ship the polyfill universally. A scroll-driven animations polyfill exists and works well, but loading it for Chrome users who don't need it erases the performance gains you're chasing. Use CSS.supports('animation-timeline', 'scroll()') in JavaScript to conditionally load it only where required.


Migrating from ScrollTrigger: A Practical Checklist

If your team has an existing GSAP ScrollTrigger implementation, here's a realistic migration path:

  1. Audit by animation type. Separate scrub-linked animations (parallax, progress bars) from trigger-on-entry animations (reveals, counters). The former maps directly to CSS scroll timelines. The latter can use view() timelines or fall back to Intersection Observer for class-toggle triggering.

  2. Flag JS-dependent animations. If an animation relies on runtime values — dynamic content heights, window dimensions, user velocity — keep it in JavaScript. CSS scroll timelines excel at design-time-defined motion, not runtime-computed offsets.

  3. Migrate one component at a time. Start with the reading progress bar. It's a guaranteed quick win. Ship it, profile it, verify the fallback, build confidence before a wholesale rewrite.

  4. Conditionally load GSAP. Once enough components migrate, wrap your GSAP import: load it only when !CSS.supports('animation-timeline', 'scroll()'). Your modern-browser bundle shrinks immediately.

  5. Profile before and after. Capture a scroll trace in Chrome's Performance panel pre- and post-migration. The drop in main thread JS execution is visceral and makes for compelling data when you're justifying the refactor to a technical lead or client.


Is It Time to Deprecate Your Scroll Libraries?

Not entirely — and not all at once. GSAP's ScrollTrigger still wins for highly sequenced multi-element choreography where JavaScript logic is already doing the heavy lifting. Complex SVG morphing synced to scroll position, scroll-jacked narrative experiences, or animations that respond to user velocity are legitimate cases where JavaScript remains the right tool.

But for the 80% of scroll animations agencies ship on every project — card reveals, parallax layers, progress indicators, section transitions — native CSS scroll-driven animations are now the superior choice on capable browsers. Faster. Simpler. Zero bundle cost.

The smart move right now is a deliberate, progressive migration: CSS scroll animations as the default for modern browsers, lightweight fallbacks for the rest, and a slow sunset of scroll library dependencies as Safari catches up.

The spec is here. The performance wins are real and measurable. The question was never whether to adopt it — only how quickly your team can move.

Start with one component. Ship it this sprint. Your main thread will finally get some rest.