Only treat a downward drag as a pull when the list is already scrolled to the very top (`scrollTop === 0`). Push the whole list down with `translateY` by the drag distance, revealing a spinner or arrow above it that rotates as you pull — multiply the raw distance by roughly 0.4–0.6 so resistance grows as you pull further, giving it a rubber-band feel.
Release past a threshold (usually 60–80px) and the list locks in place while the spinner keeps turning in a "loading" state; once the real data arrives, animate the list back to rest and insert the new item at the top.
Unlike native apps, on the web this can collide with the browser's own pull-to-refresh, so it's safer to also set `overscroll-behavior: contain` on the container.
When to use
Use it at the top of feeds and inboxes where new data arrives often. Pairs well with infinite scroll for a list that fills from both ends.