Pull to Refresh
Drag a list down from the top and let go to load the newest items. Learn when it fits, how threshold, resistance and spinner states work, what should happen on errors and offline, and how testers break it.
You’re waiting for a refund. You open your banking app, pull the transactions list down, the spinner turns… and turns… and turns. Is the bank down? Is your internet down? Did the refund arrive? Nothing tells you. You close the app, open it again, and there it is: the refund was in the response all along, but the spinner never stopped.
Pull to refresh is one of the smallest components in an app, and one of the most used. It’s also full of timing bugs: double requests, endless spinners, duplicate items and lists that jump. Let’s learn it properly.
What is pull to refresh?
Pull to refresh lets people reload a list by dragging it down when it’s already scrolled to the top, then letting go. A spinner shows while new data loads, and fresh items appear at the top.
You’ll find it in:
- Feeds: social posts, news, friends’ workouts.
- Inboxes: mail, messages, notifications.
- Time-sorted lists: bank transactions, orders and deliveries, flight status.
It’s a shortcut. Good apps also refresh automatically (when you open the screen, when the app returns from the background) and offer a visible way to refresh. Pulling is what impatient people do when they think something new should be there.
The four states
Every pull goes through the same states. Most bugs happen at the borders between them.
iOS
Android
1Pulling
The list follows your finger, with resistance. The indicator fills up to show how far is enough.
iOS
Android
2Armed
You passed the threshold. Letting go now will refresh. Often a haptic tick and "Release to refresh".
iOS
Android
3Refreshing
One request is running. The indicator spins and stays visible until it finishes.
iOS
2 new stories
Android
4Done
New items appear at the top, "Updated" time changes, and the indicator goes away. Or an error is shown.
On iOS the list itself moves down and the spinner sits in the gap. On Android the list stays still and a small disc slides down over it.
Threshold and resistance
Two numbers decide how a pull feels:
- Threshold: how far you must pull before letting go triggers a refresh. Too low and it fires by accident while you scroll up; too high and people give up. Most apps need roughly 60–120 points of pull. The indicator usually “fills up” as you approach it, and many apps give a hapticHaptic feedbackA short vibration that confirms an action without the user needing to look at the screen.Open in glossary → tick when you pass it.
- Resistance: the list moves less than your finger (often around half), so it feels like stretching a rubber band. Without resistance the list can be dragged halfway down the screen. With too much, the threshold feels unreachable.
One more rule matters: the pull only counts if the gesture started at the top. If you start scrolled down and flick back up to the top in one movement, that’s scrolling, not a request to refresh.
How it differs from similar patterns
| Pattern | What it does | Typical use |
|---|---|---|
| Pull to refresh | Loads newer items at the top on request | Feeds, inboxes, time-sorted lists |
| Refresh button | The same, without a gesture | Toolbars, desktop web, the accessible alternative |
| Auto refresh / live updates | New items arrive by themselves | Chats, live scores, order tracking |
| “New posts” pill | Tells you new items are waiting above | Long feeds where you’ve scrolled down |
| infinite scrollInfinite scrollLoading more items automatically when the user reaches the bottom of a list.Open in glossary → / Load more | Loads older items at the bottom | Long lists, search results |
Try it: refresh lab
This news app does pull to refresh correctly on iOS and Android. Play with the threshold and resistance, then try to break it: pull during a slow refresh, scroll down and flick back to the top, switch to Server error and Offline, and use the refresh button instead of the gesture. Watch the network log.
Headlines
Updated 8:15
City council approves new bike lanes on the river road
Metro Desk · 8:10
Rail strike called off after late-night talks
Transport · 7:55
Heatwave warning for the weekend: what to know
Weather · 7:30
Local bakery wins national sourdough award
Food · 7:02
New ferry timetable starts on Monday
Transport · 6:45
Library extends opening hours during exams
Education · 6:20
Stadium concert: road closures and parking tips
Metro Desk · 6:05
Startup week brings 40 free workshops to town
Business · 5:50
Platform
Threshold
Resistance
Network
Pull: 0 px of 70 px needed. The red line is the threshold.
Network log
- Pull down from the top of the list…
Try: pull during a slow refresh. Scroll down, then drag back up to the top in one go (it should not refresh). Switch to Server error and Offline. Use the refresh button with the keyboard.
Dev note
On iOS it’s a refresh control (UIRefreshControl in UIKit, .refreshable in SwiftUI). On Android it’s swipe to refresh (SwipeRefreshLayout for Views) or pull to refresh (PullToRefreshBox in Jetpack Compose Material 3). On the web, mobile browsers have their own pull to refresh that reloads the whole page.
When to use it
Pull to refresh is a good choice when:
- The list is sorted by time, newest first, so “new” has an obvious place: the top.
- Content changes often and people check back for updates: inbox, feed, order status.
- The data might be stale even after auto refresh (on a weak connection, after a long time in the background).
Good and bad uses
Feeds, inboxes, transactions
A list of orders sorted by date. Pulling down checks for new deliveries and updates the statuses.
Forms and editable screens
A checkout form or a profile editor. A refresh would throw away what the user typed, and there’s nothing “new” to load. Scrolling up at the top of a long form shouldn’t risk that.
Newest at the top
A news list or a notification list, where new things appear above old ones.
Newest at the bottom
A chat conversation. New messages arrive at the bottom in real time, and pulling down at the top usually means “load older messages”. Pull to refresh there would confuse both users and code.
Simple vertical lists
A list that scrolls vertically and has nothing else that reacts to a downward drag.
Maps, canvases and sheets
A map pans when you drag down. A bottom sheet closes when you drag down. A drawing canvas draws. Adding pull to refresh to these makes gestures fight.
How good pull to refresh behaves
One request at a time
While a refresh is running, pulling again should not start a second request. The app either ignores the new pull or keeps showing the spinner. The same goes for a refresh while the screen is still doing its first load, or while it’s loading the next page at the bottom. Two overlapping requests are the classic source of duplicates and of a race conditionRace conditionA bug that depends on the timing or order of events, so it only happens sometimes.Open in glossary → where an older response overwrites a newer one.
New items, no duplicates, no jumps
- When the user pulled from the top, the new items appear at the top, and the list shows them.
- Items that were already there are not duplicated. The app should merge by item ID, not just stick the response on top.
- Items that changed (an order went from “On the way” to “Delivered”) update in place.
- The list doesn’t jump to a random position. If an app auto-refreshes while you’re reading further down, it should keep your place and show a “3 new posts” pill instead of moving the content under your thumb.
Errors stop the spinner
When the request fails, the spinner must stop. The old content stays on screen, and the user gets a clear, polite error messageError messageText that tells the user something went wrong and, ideally, how to fix it.Open in glossary →: a toast or an inline banner like “Couldn’t load new orders. Retry”. An endless spinner is the worst outcome: the user can’t tell whether to wait, retry or give up. Also check what happens on a timeoutTimeoutThe time limit after which an app stops waiting for a response and gives up.Open in glossary →: a request that never answers should end in an error after a reasonable time.
Offline is not an error you wait for
When the device is offline (airplane modeAirplane modeA device setting that turns off all wireless connections at once.Open in glossary →, no signal, a captive portalCaptive portalA Wi-Fi login page that intercepts traffic until the user signs in or accepts terms.Open in glossary →), good apps answer immediately: “You’re offline. Showing saved orders.” They keep the cached list and don’t pretend to load. See Network conditions for how to simulate this.
“Last updated” tells the truth
Many apps show “Updated 9:41” or “Updated just now” under the title or in the indicator. It must change after every successful refresh, and must not change after a failed one. A stale timestamp makes users distrust everything else on the screen.
It plays nicely with other gestures
Pulling only starts at the top. Horizontal swipes on rows (swipe actions), carousels inside the list, and the system’s own gestures must still work. On the web, the app’s pull to refresh must not fight the browser’s pull to refresh.
Common bugs
| # | Bug | What you see | Typical severity |
|---|---|---|---|
| 1 | Endless spinner | On an error or offline, the spinner never stops; no message | High |
| 2 | Double request | Pulling again during a refresh sends another request | Medium |
| 3 | Duplicate items | The same posts or transactions appear twice after refreshing | High |
| 4 | List jumps | After a refresh you’re in the middle of the list, not at the new items | Medium |
| 5 | Fires by accident | Normal scrolling up to the top triggers a refresh (threshold too low or wrong start rule) | Medium |
| 6 | Never fires | The threshold is unreachable, or the pull doesn’t work on short lists that don’t scroll | Medium |
| 7 | Stale “Updated” time | Still says 8:15 after a successful refresh, or updates after a failed one | Low–Medium |
| 8 | No alternative | No refresh button or menu; keyboard, switch and voice users can’t refresh | High (for affected users) |
| 9 | Lost input | A refresh wipes a half-typed comment or a filter the user set | Medium–High |
| 10 | Browser reloads the page | On a mobile website, pulling reloads the whole page instead of the list | Medium |
Find the bug
This fitness app shows your friends’ activity with pull to refresh, and it has six bugs. Each request takes about 2.5 seconds. Explore it like a tester:
- Pull to refresh once and watch the list, the spinner, the header and the network log.
- Pull again while the spinner is turning.
- Scroll down a bit, then drag the list back up to the top in one movement.
- Use the device panel: turn on server errors or airplane mode, then pull.
- Try to refresh without dragging.
🐞 Bug hunt: Activity feed
This demo has 6 hidden bugs. Explore it like a tester, then check your findings.
Activity
Friends · Updated 8:15
- EEmma ran 5.2 km28:14 · Riverside loop8:02
- DDaniel cycled 22 km54:10 · Canal path7:48
- SSofia swam 1.5 km36:40 · City pool7:30
- LLeo hiked 9.8 km2:41:00 · Green hills7:05
- MMia ran 10 km52:31 · Park run6:50
- MMarco cycled 41 km1:32:00 · Coast road6:31
- AAnna walked 4.1 km45:12 · Old town6:10
- TTom swam 800 m22:05 · LakeYesterday
- LLena ran 7.4 km39:50 · Hill repeatsYesterday
- OOmar hiked 14 km4:05:00 · Ridge trailYesterday
Device
Drag the list with your mouse or finger to scroll and to pull down at the top. Each request takes about 2.5 s.
Network log
- Pull to refresh to see API requests…
- 1
Hint: Turn on server errors (or airplane mode) and pull to refresh.
- 2
Hint: Pull, then pull again before the first refresh finishes. Watch the network log and the top of the feed.
- 3
Hint: Pull once from the top and watch where you end up when the new items arrive.
- 4
Hint: Scroll down a little, then drag the list down to scroll back up, past the top.
- 5
Hint: Look for a refresh button or menu item. Try with the keyboard only.
- 6
Hint: Refresh successfully and read the header.
How to test it
Checklist
Pull to refresh test checklist
0 of 14 checked
Example test cases
TC-PTR-001Pull past the threshold loads new itemspositivehigh
Preconditions
- Server has 3 new activities
- Feed is scrolled to the top
Steps
- Pull the feed down past the threshold
- Release
- Wait for the spinner to disappear
Expected result
Exactly one request is sent. The 3 new activities appear at the top, the list stays at the top, and the header says "Updated just now".
TC-PTR-002Short pull does not refreshnegativemedium
Steps
- Scroll to the top
- Pull down about 1 cm and release
Expected result
The list snaps back. No request is sent.
TC-PTR-003Pulling during a refresh sends no second requestedgehigh
Preconditions
- Network throttled to "Slow 3G" or a proxy breakpoint holds the response
Steps
- Pull to refresh
- While the spinner is visible, pull to refresh again
- Release the response
Expected result
Only one feed request is sent. New items appear once; nothing is duplicated.
TC-PTR-004Server error stops the spinner and offers Retrynegativehigh
Preconditions
- Proxy tool returns HTTP 500 for the feed request
Steps
- Pull to refresh
- Wait 5 seconds
Expected result
The spinner stops. The previous items are still shown. A message like "Couldn't refresh. Retry" appears, and "Updated" keeps its old time.
TC-PTR-005Offline refresh answers immediatelynegativemedium
Preconditions
- Airplane mode is on
Steps
- Pull to refresh
Expected result
Within a second the app says it is offline and keeps showing cached content. No endless spinner.
TC-PTR-006Scrolling back to the top does not refreshedgemedium
Steps
- Scroll the feed down by about one screen
- Drag (or flick) the list down to scroll back up, overshooting the top
Expected result
The list stops at the top (maybe with a small bounce). No request is sent.
TC-PTR-007Refresh is possible without the gesturepositivehigh
Preconditions
- Voice Control (iOS) or a hardware keyboard is used
Steps
- Open the feed
- Activate the Refresh button (say "Tap Refresh" or Tab to it and press Enter)
Expected result
The feed refreshes exactly like after a pull, and the result is announced.
TC-PTR-008Mobile web: browser does not reload the pageedgemedium
Preconditions
- Chrome on Android
- Web app has its own pull to refresh on the feed
Steps
- Scroll the feed to the top
- Pull down past the threshold
Expected result
Only the app's own refresh runs. The browser does not reload the page, and typed text elsewhere on the page is kept.
Tip: click a test case to expand it.
Tools
- Slow it down: network throttlingNetwork throttlingDeliberately slowing the connection to test how an app behaves on poor networks.Open in glossary → (Network Link Conditioner on iOS, the emulator’s network settings on Android, DevTools on the web) gives you time to pull twice.
- Hold or break responses: a proxy toolProxy toolA program that sits between the app and the server so you can watch and change network traffic.Open in glossary → like Charles or Proxyman can pause a response with a breakpoint, return a
500, or add a long delay to check timeoutTimeoutThe time limit after which an app stops waiting for a response and gives up.Open in glossary → handling. See Proxy tools. - Go offline: airplane mode on a real device, or Offline in Chrome DevTools’ Network panel.
- Watch the requests: count them. One pull, one request. Check the query (
since=,page=, cursor) to see whether two requests asked for the same thing. - Precise pulls on Android:
adb shell input swipe 540 600 540 1100 400drags down 500 px in 400 ms; change the distance to find the threshold. See ADB basics.
Accessibility
Pull to refresh is a dragging gesture, so some people can’t do it at all: people with motor impairments, people using Switch Control, Voice Control or a keyboard. Check these:
- A visible alternative: a Refresh button in the toolbar, a menu item, or automatic refresh when the screen opens. WCAGWCAGThe Web Content Accessibility Guidelines, the main international standard for accessibility.Open in glossary → 2.5.1 Pointer Gestures (Level A) and 2.5.7 Dragging Movements (Level AA) ask for a way to do gesture and drag actions with a simple tap or click instead.
- Screen readers: VoiceOverScreen readerSoftware that reads the screen aloud for blind and low-vision users, like VoiceOver or TalkBack.Open in glossary → users scroll with three fingers. With a standard iOS refresh control, a three-finger swipe down at the top of the list often triggers the refresh, but not reliably, and custom implementations often don’t support it at all. TalkBack users need a Refresh button or action. Test the real thing, not the documentation.
- Announce the state: “Refreshing”, then the result: “3 new stories” or “Couldn’t refresh”. The spinner needs an accessible label, and the result should be announced without moving focus.
- Don’t move focus: when new items arrive at the top, focus should stay on the item the user was on.
- Reduced motion: with Reduce Motion on, the refresh should still work and be visible, just with less animation.
- Keyboard (web): the Refresh button is reachable with Tab, has a visible focus ring and a clear name (“Refresh orders”, not just an icon).
Mobile vs web
- The spinner stays until the work finishes, so a request that never ends means a spinner that never ends.
Dev note
Built in:UIRefreshControl(UIKit) and.refreshable(SwiftUI). With.refreshable, the spinner stays until the async work finishes. - The list moves down and the spinner sits in the gap; a haptic tick can mark the threshold.
- Test with VoiceOver’s three-finger scroll, Voice Control and Reduce Motion.
Dev note
SwipeRefreshLayout (Views) and PullToRefreshBox (Compose Material 3).- A small circular indicator slides down over the list; the list itself usually stays put. Android 12+ also shows a “stretch” overscroll effect elsewhere.
- Check that the indicator doesn’t clash with the top app bar or tabs, and that TalkBack users have a Refresh action.
- Mobile browsers have their own pull to refresh that reloads the whole page (Chrome on Android, Safari on iOS 15 and later).
- A web app with its own pull to refresh has to stop the browser’s version. Support differs between browsers and versions, so test on a real iPhone and Android phone.
Dev note
It uses the CSSoverscroll-behaviorproperty (for exampleoverscroll-behavior-y: contain). - Desktop has no pull at all: the page needs a visible Refresh button (and browser reload must not lose the user’s work).
Check your understanding
Quick quiz
0/5 answered · 0 correct
1.Where does pull to refresh fit best?
2.You pull twice quickly and every new post appears twice. What is the most likely cause?
3.The server returns an error during a refresh. What should the user see?
4.You scroll down, then drag the list back up to the top in one movement, and it refreshes. Which rule is broken?
5.Why does a pull-to-refresh screen need a visible Refresh button too?
Key takeaways
- Pull to refresh reloads time-sorted lists (feeds, inboxes, transactions); avoid it on forms, maps, sheets and bottom-anchored chats.
- It moves through pulling → armed → refreshing → done (or error); the pull must start at the top and pass a deliberate threshold.
- Test the timing: one request at a time, no duplicates, no jumps, and a spinner that always stops, including on errors and offline.
- “Last updated” must tell the truth.
- Always provide a visible Refresh and announce the result for screen reader users.
Finished this page?Nice work, this page is complete!
Mark it as done to track your progress. It's saved in this browser, no account needed.
Its review cards are now in Bites ⚡