1 slot open this monthBy the makers of shadcnregistryWe built shadcnregistry. Now we build your product.Book a callCheck availability
1 slot open this monthAI features?We build AI features into your product.Book a callCheck availability
1 slot open this month🚨 Slop police🚨 Slop police: we clean up vibe-coded apps.Report itReport your app
shadcnregistry
shadcnregistry
RegistryHealthInspect
Terms of ServicePrivacy Policy
  1. Registries
  2. pulld
  3. Pull To Refresh

Pull To Refresh

ui

A list that reloads when it is pulled down from the top, the way a native app does, plus a Refresh button that always does the same job. The indicator follows the finger one-to-one until releasing would refresh — so "release to refresh" appears exactly when the finger has gone the distance the label implies — then tapers towards a ceiling it never reaches, so the list cannot be dragged off the bottom of the screen. Reach for it on a mobile web list people expect to behave like an app: a PWA's inbox or feed, a notifications or activity list, chat or message threads, an orders or deliveries screen, stock and price panels that go stale while they are looked at, a dashboard on a phone, a queue or ticket list a support agent keeps checking, and search results after the network came back. Common asks it answers: "pull to refresh react", "pull to refresh web", "swipe down to refresh", "pull down to reload a list", "pull to refresh shadcn", "pull to refresh without a library", "react-simple-pull-to-refresh alternative", "react-pull-to-refresh alternative", "pwa pull to refresh", "mobile web refresh gesture", "disable the browser's own pull to refresh", "two spinners when I pull to refresh on android", "overscroll-behavior contain", "Unable to preventDefault inside passive event listener invocation", "onTouchMove preventDefault not working in react", "pull to refresh broken on ios", "ios rubber band scrollTop negative", "pull to refresh fires while the list is still scrolling", "accessible pull to refresh", "refresh a list without a pull gesture". Official shadcn/ui has nothing for this, and nothing to build it out of: touchstart, touchmove, touchend, touchcancel, TouchEvent, clientY, pullDistance, refreshing, onRefresh and overscroll-behavior are each zero hits across all 62 of its fetchable items (measured 2026-09-27) — it does not handle a touch anywhere. The two overscroll hits are both Tailwind classes and neither is a refresh: overscroll-x-contain on the attachment strip, and overscroll-contain on message-scroller, a chat log that autoscrolls downward and reads no gesture at all. Distinct from the pulld pieces it sits beside: infinite-scroll is the bottom of the same list and loads the next page, this is the top of it and replaces the first; scroll-progress reports how far down the page has gone; scroll-shadow says there is more to see sideways; network-status says the connection dropped, and this is what someone does about it once it is back; save-status reports writes going out rather than reads coming in. Six things separate it from the version built out of onTouchMove and a clientY subtraction. First, the scroller is this component's own element, so overscroll-behavior-y: contain is not something a consumer has to remember: without it Chrome on Android runs its own pull-to-refresh over the top of this one and a single gesture shows two spinners. It is contain and not none, so the rubber-band bounce that tells people they have reached the end survives. Second, touchmove is registered by hand with passive: false, because React registers onTouchMove passively and a passive listener's preventDefault is dropped with a console warning — the indicator comes down and the browser overscrolls behind it, and no prop changes that. touchstart, touchend and touchcancel stay React props precisely because none of them ever has to prevent anything. Third, a negative scrollTop is refused rather than welcomed: it is iOS saying it is already sliding the content down, and a translate added on top of its bounce moves the list twice as far as the finger. Whether a gesture may pull at all is settled once, at touchstart, and thereafter only ever taken away — an implementation that asks whether scrollTop is 0 on every move hands out pulls in the middle of scrolls, because a list gliding to a halt passes through 0 with a steadying finger already resting on it. Being at rest is measured as time since the last scroll event, which is the only test that works in engines that hold scrollTop at 0 throughout their bounce. "At the top" is a one-pixel window, not a comparison against zero, because browser zoom and fractional device pixel ratios park a scroller at 0.5. Fourth, the gestures that are not pulls are each turned away: a second finger (a pinch-zoom starting inside the list), a sideways drag (a carousel or a swipeable row — ties go to horizontal, since refusing a pull costs one more try while stealing a swipe breaks what is under the finger), a drag upward, which ends the gesture rather than waiting at zero for the finger to drift back down and become a pull mid-scroll, and a move the browser has already marked non-cancelable. An axis once decided is never re-decided, so the arc a thumb actually describes does not kill the pull half way down. Fifth, the gesture is touch-only, and to a keyboard, a switch, a desktop mouse or a screen reader a list whose sole way to reload is a pull cannot be reloaded: so a Refresh button is always rendered, it carries aria-disabled rather than disabled so pressing it never drops focus out of the list, every stage is announced through a polite live region that is in the DOM before it has anything to say, the scroller gets aria-busy, and the indicator itself is aria-hidden because it is a drawing of where a finger is. Sixth, it cannot get stuck: one refresh runs at a time whether it came from the gesture or the button, a rejected promise and a loader that throws synchronously both announce the failure and let the next attempt through instead of leaving the spinner turning for the session, a touchcancel — an incoming call, the system's own edge gesture — closes without refreshing rather than sharing the release handler and refreshing on an interruption nobody released, and nothing is set after unmount. Controlled by one prop: pass onRefresh and give the scroller a height with scrollClassName. Optional threshold (default 64px), maxDistance, disabled, hideButton for callers supplying their own control, and labels to reword or translate every string. Styled with shadcn tokens so it follows light and dark themes and honours prefers-reduced-motion; lucide-react is the only dependency, with no Radix, no gesture library and no react-dom import.

pnpm dlx shadcn@latest add "https://pulld.pages.dev/r/pull-to-refresh.json"
Premium blocks for shadcn/uiShadcnblocks.com · AffiliateExplore
  • lucide-react
    components/ui/pull-to-refresh.tsx