Scroll Animations in Pure CSS: Time to Drop IntersectionObserver
By Arash Latifi
Scroll-driven animations shipped everywhere via Interop 2026, and scroll-triggered animations are an Interop 2027 candidate. Here's what you can delete from your JS today.
TL;DR — Scroll-driven animations are now supported in all modern browsers (Interop 2026), and scroll-triggered animations are up for Interop 2027 — so a chunk of your
IntersectionObserverJavaScript can go.
Two concepts people keep confusing
Josh W. Comeau's "The Future of CSS" (October 2026) draws the line clearly:
- Scroll-driven animations: the animation is scrubbed by your scrolling — think an element fading in as it traverses the viewport. Part of Interop 2026, now implemented across all browsers.
- Scroll-triggered animations: reaching a scroll point triggers an animation that then runs normally over its duration — like a proximity sensor going off. Still a proposal, an Interop 2027 candidate.
The current tool for the second kind is IntersectionObserver in JavaScript — which Comeau describes as "kind of a pain." Per MDN, pre-IO approaches (and even main-thread intersection checks in loops with getBoundingClientRect) made scrolling sluggish; IO's win was running callbacks off the scroll path. CSS now removes the need for it entirely for driven animations.
Scroll-driven: the basics
You need three things: a @keyframes rule, an animation-timeline, and an animation-range. Fading in a product card as it enters the viewport:
view() ties the timeline to the element's own position in the viewport; animation-range: entry 0% entry 60% means the animation runs from first entry until the element is 60% in. The both fill mode prevents an initial flash of the unanimated state.
For a progress bar tied to the whole page, use scroll() instead:
Key detail: in scroll-driven mode, duration is meaningless — scroll speed is the animation speed, which is why linear is almost always the right easing.
Scroll-triggered: what to do today
Until the proposal lands everywhere, you can fake one-shot behavior with a narrow range and both:
Because the timeline replaces real time, this doesn't behave exactly like an IO callback — if you need "run once, on its own clock, after entering," keep IntersectionObserver for now (or test the proposed animation-trigger behind a flag). The new feature is still experimental: test in staging, not production.
Which technique for which case?
| Case | Right tool | Support (October 2026) |
|---|---|---|
| Scrubbed effects (parallax, progress bar) | animation-timeline: scroll()/view() | All modern browsers (Interop 2026) |
| Trigger-once on arrival | scroll-triggered / animation-trigger | Proposal, Interop 2027 candidate |
| Lazy-loading images | loading="lazy" + IO fallback | Native |
| "Reveal then bounce" sequences | Still JS with IO | Until triggers ship |
Practical notes
- Always include
bothso elements aren't invisible/displaced before their timeline starts. - Plan a fallback: without support, animations either don't run or stick at their initial state — use
@supports (animation-timeline: view())to define an acceptable default. view()also works inside scrollable containers — getscroll-scoperight.- If you pull in GSAP or Framer Motion only for simple reveals, you may be able to drop the whole library — smaller bundles, better LCP.
Migration checklist
- [ ] Categorized every IO usage: scrub or one-shot trigger?
- [ ] Moved scrub cases to
animation-timeline: view()/scroll() - [ ] Kept true triggers on IO (or tested
animation-triggerbehind a flag) - [ ] Added
@supportsfallbacks for older browsers - [ ] Animations disabled under
prefers-reduced-motion - [ ] Actually tested in Chrome, Safari, and Firefox
FAQ
Q: Does scroll-driven work in all browsers?
Per Comeau, it was part of Interop 2026 and has been successfully implemented across all major browsers. Still ship @supports fallbacks for older versions.
Q: Can I use scroll-triggered animations today?
They exist as a proposal and an Interop 2027 candidate — test behind flags, don't rely on them in production. IntersectionObserver remains the correct tool for now.
Q: Does dropping IO actually improve performance?
IO was already the light option compared to scroll-handler getBoundingClientRect loops. The real win of pure CSS is less JavaScript and simpler code, not a dramatic FPS jump.