Detecting When Scrolling Stops in JavaScript (and React)
Have you ever needed to know when a user stops scrolling? It sounds simple, but as I discovered, it’s trickier than it looks. Between new browser events like scrollend, programmatic scrolls with window.scrollTo, and edge cases across devices, it’s easy to get stuck in loops or fire too many callbacks.
In this post I’ll walk through some of the problems I hit, and the different approaches you can take to reliably detect “scroll end.”
The Problem: Knowing When Scrolling Ends
The scroll event is easy enough — it fires constantly while the user scrolls. But what I wanted was a debounced “scroll has finished” event that I could use in a React app. Think of things like:
- Hiding a floating toolbar until the user stops moving.
- Triggering animations after scrolling completes.
- Tracking engagement when a reader settles on a section.
First Attempt: Framer Motion useScroll
Since I was already using Framer Motion, my first try was to hook into its useScroll. It provides reactive values like scrollY, and I wrapped it with a little timeout logic:
This worked well enough, but it’s essentially just debouncing the raw scroll events.
Enter the scrollend Event
More recently, browsers have been shipping a native scrollend event. Perfect! In theory I could just do:
But two problems cropped up:
- Support isn’t universal — Safari, for example, doesn’t fire it yet.
- Programmatic scrolls also trigger
scrollend— so callingwindow.scrollToended up firing my callback and creating infinite loops.
Distinguishing User vs. Programmatic Scrolls
To solve the loop issue, I explored a few approaches:
- Flagging programmatic scrolls
WrapscrollTocalls in a helper that sets a boolean, and ignorescrollendwhile it’strue. - Comparing scroll positions
Before callingscrollTo, check ifwindow.scrollYis already within a pixel or two of the target. If so, skip it — no need to trigger an event.function safeScrollTo(targetY: number) { if (Math.abs(window.scrollY - targetY) < 1) return; window.scrollTo({ top: targetY, behavior: "smooth" }); } - Looking at user input
Track recentwheel,touchmove, or arrow key events. Ifscrollendfires without a recent user action, assume it was programmatic.
Polyfilling scrollend
Since support is patchy, I also considered simulating the event myself. That way, consumers of my code could always just listen to scrollend — whether real or polyfilled.
Now my app code doesn’t need to know the difference.
Wrapping It Up in React
For React apps, it’s convenient to encapsulate this into a hook:
Takeaways
- Native
scrollendis coming, but you can’t rely on it everywhere yet. - Programmatic scrolling complicates things — add guards so you don’t fire false “user” events.
- A polyfill approach lets you treat everything consistently and just listen for one event.
- In React, wrapping the whole thing into a custom hook keeps your components clean.
👉 If you’re dealing with scroll-based interactions today, I’d recommend starting with a polyfill + hook. As browser support improves, you can let the native event take over automatically.