Infinite Scroll
New items load by themselves as you reach the bottom of a list. Learn how the sentinel, prefetch distance and cursors work, what the loading, error and end states must look like, and how testers find duplicates, lost positions, unreachable footers and keyboard traps.
You’re looking for hiking boots. You scroll through 80 products, spot a nice pair, tap it, read the details and press Back. You’re at the top of the list again, with 12 products. The other 68 are gone, and so is your place. You try to find the Returns policy in the footer instead, but every time you get close, twelve more products push it away. You close the tab.
infinite scrollInfinite scrollLoading more items automatically when the user reaches the bottom of a list.Open in glossary → feels effortless when it works. Underneath, it’s a little machine: something watches the end of the list, requests the next page, appends it, and has to handle slow networks, errors, the end of the data, the Back button and people who don’t use a mouse. Every part of that machine has its own bugs.
What is infinite scroll?
Infinite scroll loads the next batch of items automatically when the user nears the bottom of a list. There’s no “Next” button: scrolling is the action. You know it from social feeds, photo streams, news apps, shop categories and search results in many apps.
It’s still pagination underneath. The app asks the server for a page of items (say 20), shows them, and asks for the next page when it’s needed. The difference is only who decides when: the scroll position, not a tap.
Anatomy
One list, three possible bottoms: loading, error or the end
- 1
Loaded items
Everything fetched so far. Each request adds one more page (often 10–30 items).
- 2
Viewport
What the user sees right now. Everything else is above or below it.
- 3
Prefetch distance
How early the next page starts loading, e.g. one screen before the end, so people rarely wait.
- 4
Sentinel
An invisible marker after the last item. When it comes close to the viewport, the next page is requested.
- 5
Loading row
A small spinner at the bottom while the next page loads. Must disappear on success, error and at the end.
- 6
Error row
“Couldn’t load more. Retry” right where the next items would be, without losing the loaded ones.
- 7
End of list
“You’ve seen all 120 results.” Stops the spinner and makes the footer reachable.
How it works
- A sentinel marks the end. After the last loaded item sits an invisible element, the sentinel.
Dev note
On the web, anIntersectionObserverwatches it; on iOS and Android, the list reports when its last rows are about to appear (prefetchDataSourceorwillDisplayin UIKit,.onAppearon the last row in SwiftUI, the Paging library on Android). - The prefetch distance decides how early. The observer can fire before the sentinel is visible, e.g. one screen ahead. Too small, and people stare at a spinner at the bottom. Too large, and the app loads pages nobody will see.
Dev note
On the web:rootMargin: "0px 0px 800px 0px". - One request asks for the next page. Feeds usually use cursor paginationCursor paginationFetching the next items after a given item instead of skipping a count of items.Open in glossary →: “20 items after item r40” (
?limit=20&after=r40), instead of “skip 40, take 20”. The response contains the items and the cursor for the next page, ornullwhen there’s nothing more. - The app appends and waits. New items are added at the bottom, the sentinel moves down with them, and nothing happens until it comes close again. While a request is running, the app must not start another one.
- The list ends, or fails, clearly. When the next cursor is
null, the app stops loading and says so. When a request fails, it shows an error row with Retry, right where the new items would have appeared.
The page size matters too: 10–30 items is typical. Tiny pages mean many requests and frequent waits; huge pages mean slow responses and lots of memory on cheap phones.
How it differs from similar patterns
| Pattern | What triggers loading | Typical use |
|---|---|---|
| Infinite scroll | Reaching the end of the list | Feeds, photo streams, browsing |
| “Load more” button | A tap or click | Shop lists, search results, comments |
| Numbered pagination | Choosing a page | Shops, tables, results people come back to |
| Pull to refresh | Pulling down at the top | Loading newer items at the top |
| lazy loadingLazy loadingLoading images or content only when the user scrolls near them.Open in glossary → (images) | An image nearing the viewport | Saving data on long pages; the items already exist |
| Virtualization | Not loading at all: rendering only visible rows | Keeping long lists fast in memory |
Designs & variants
- Endless feed (list): one column of posts or articles, newest first. Social apps, news.
- Product or photo grid: two to four columns. Each page fills whole rows, or the last row looks broken.
- Reverse list (chat): older messages load at the top when you scroll up, and the list must keep your place while they’re inserted above you.
- Hybrid “load more”: the first two or three pages load on scroll, then a Load more button appears. It keeps the footer reachable and gives keyboard users a stop.
- With helpers: a “Back to top” button after a long scroll, a “New posts” pill when fresh items wait above, or a result count (“312 results”).
Try it: scroll lab
A correct infinite scroll on a recipes feed. The red dashed line is the sentinel, the blue one shows where loading starts. Play with the settings and watch the counters and the network log:
- Scroll to the bottom with prefetch 0 px (you wait for the spinner), then with 1 screen (you rarely see it).
- Switch the network to Fails, scroll down, then back to Fast and tap Retry. Nothing loads on its own after an error until you retry.
- Use 45 recipes to reach the end and the footer. Use 1,000, load a few pages and turn on Virtualize: the rows in the DOM drop to a handful.
- Open a recipe far down the list and go Back. Try Load more button mode with the keyboard.
Recipes
0 of 45 loaded
Prefetch distance
With a slow network, 0 px means waiting at the bottom; 1 screen loads before you get there.
Page size
Items per request: see limit= in the log.
Network
Catalog size
Restarts the feed. 45 reaches the end quickly.
Loading
Virtualize rows
Watch “Rows in the DOM” below, best with 1,000 and a few loads.
Debug overlay
Loaded
0
Rows in the DOM
0
Sentinel distance
0 px
Network log
Try: scroll to the bottom with “0 px” and then with “1 screen”. Switch the network to Fails, then back to Fast and tap Retry. Open a recipe far down and go Back.
When to use it
Infinite scroll fits when people browse without a specific goal:
- Feeds and streams: social posts, news, photos, short videos, activity.
- Discovery lists: “Inspiration”, “For you”, a category people scroll through for fun.
- Mobile lists where tapping a tiny “Next” would be awkward, and nothing important lives below the list.
Good and bad uses
Browsing a feed
A photo feed. Nobody needs to come back to “the 213th photo”, and there’s no footer.
Finding and comparing
Search results or a shop category where people compare, open items and come back. They need a sense of position, working Back, and shareable links: pagination or Load more suits them better.
Footer links elsewhere
A feed app where Help, Contact and Privacy live in a menu or settings screen.
A footer below an endless list
A web shop with Contact, Returns and Imprint in the footer. The footer runs away every time you scroll toward it.
Long lists, short tasks
A news feed people scroll for a few minutes.
Goal-oriented or regulated lists
Bank transactions for a tax year, an admin table of 10,000 orders. People need totals, filters, exports, and to know where the list ends.
How good infinite scroll behaves
One request per page, no duplicates or gaps
Only one request may run at a time. The classic bug: the trigger fires, the user keeps scrolling while the page loads, the trigger fires again (a throttle or debounce doesn’t help if the request is slower than it), and the same cursor is requested twice. The page then shows the same items twice. Good apps keep an “already loading” flag and also merge results by item ID.
Data also changes while people scroll: new posts arrive, items sell out. With offset paging (“skip 40”) that causes duplicates and skipped items; cursor paging avoids most of it. The Pagination page has a live demo of both. Test it by adding or deleting items (with a second device or an admin tool) while you’re halfway down the list.
A small loading row, not a full-screen spinner
While the next page loads, a short row with a spinner at the bottom (“Loading more…”) is enough. The items already on screen stay usable. With a good prefetch distance, people on a normal connection rarely see it at all. See Progress & spinners for indicator timing.
Errors stay inline, with Retry
If a page fails, the loaded items stay, and an error row appears at the bottom: “Couldn’t load more. Retry”. Two common bugs are opposites of each other:
- Silent dead end: the spinner disappears, nothing more ever loads, and nothing says why. Users think they’ve reached the end.
- Retry storm: the app retries automatically on every scroll event and floods the server (and the battery). Automatic retries need limits and a growing delay (retry with backoffRetry with backoffAutomatically trying a failed request again, waiting longer between each attempt.Open in glossary →).
The end is an end
When the server says there are no more items, the spinner stops and a message says so: “You’ve seen all 45 recipes” or “You’re all caught up”. Without it, users keep scrolling and waiting. Test the end with a short list too: a filter or a category with only 7 items is the fastest way to reach it.
Back returns to the same place
After opening an item and going Back, the list must look exactly as before: the same loaded items, the same scroll position, the same filters. On iOS and Android this usually works when the list stays in memory, and breaks after the OS kills the app in the background (state restorationState restorationBringing the user back to the same screen and data after the OS closed the app in the background.Open in glossary →), on rotation or when the app reloads the list “to be safe”. On the web, the browser restores the position for normal page loads, but a single-page app that rebuilds the list from page 1 has to store the loaded pages and position itself.
Deep links and refresh
Infinite lists are hard to link to. Good web apps write the position into the URL (?page=4 or ?after=r80) or an anchor for the item, so Refresh, Back and shared links land near the same spot instead of the top. In apps, a deep linkDeep linkA link that opens a specific screen inside an app instead of just its home screen.Open in glossary → to an item opens the item, and Back goes somewhere sensible, not an empty list.
The footer stays reachable
If a footer is below the list, people can’t reach it: each time they get close, more items load. Fixes: switch to Load more after a few pages, put the footer links in a menu, or stop auto-loading at a fixed limit. As a tester, try to click every footer link on a page with infinite scroll.
Thousands of items stay fast
A list that has loaded 2,000 items can get slow, janky and memory-hungry, especially on older phones. Native lists only create views for visible rows. On the web, apps use virtualization: only the rows near the viewport exist in the page, the rest are empty space. It’s a performance fix with side effects: the browser’s Find in page can’t find rows that aren’t rendered, and screen readers may not see the whole list. Test long sessions: scroll for a few minutes, then check speed, memory and that scrolling back up still shows the right items.
Dev note
UITableView, UICollectionView, SwiftUI List, RecyclerView, Compose LazyColumn.Common bugs
| # | Bug | What you see | Typical severity |
|---|---|---|---|
| 1 | Duplicate page | The same items appear twice after scrolling fast | High |
| 2 | Lost position | Back from an item returns to the top with only the first page | High |
| 3 | Endless spinner at the end | No “that’s all” message; the loading row never goes away | Medium |
| 4 | Silent dead end | A failed request stops loading forever, with no message or Retry | High |
| 5 | Unreachable footer | Contact, Returns, Imprint run away as you scroll | Medium–High |
| 6 | Focus jumps | After new items load, keyboard focus goes back to the top | High (for affected users) |
| 7 | Never loads | Short screens or big monitors: the first page doesn’t fill the screen, so no scroll happens and nothing more loads | Medium |
| 8 | Retry storm | Hundreds of requests per minute after an error | High |
| 9 | Slow after a while | Jank, memory warnings or a crash after a few hundred items | Medium–High |
| 10 | Skipped items | Items added or removed during scrolling are missing (offset paging) | Medium |
Find the bug
This outdoor shop loads products as you scroll, and it has six bugs. Each request takes about 1.3 seconds. Explore it like a tester:
- Scroll down quickly and keep scrolling while “Loading more…” is shown. Then look closely at the products and the network log.
- Scroll a long way, open a product, then press Back to products.
- Open the Sale category and scroll to the bottom. Wait.
- Turn on server errors, scroll down, then turn them off again and keep scrolling.
- Try to open Contact or Imprint in the footer of All products.
- Click All products, then use only the Tab key to move through the products.
🐞 Bug hunt: Outdoor shop
This demo has 6 hidden bugs. Explore it like a tester, then check your findings.
All products
Test setup
Scroll inside the page with the mouse wheel, trackpad or finger. Keyboard: click “All products”, then use Tab. Each request takes about 1.3 s.
Network log
- Scroll to see API requests…
- 1
Hint: Scroll to the bottom and keep scrolling while “Loading more…” shows. Compare the network log with the products.
- 2
Hint: Scroll down past a few loads, open a product, then click Back to products.
- 3
Hint: Open the Sale category and scroll to the bottom. Check the response in the network log.
- 4
Hint: Turn on server errors, scroll to the bottom, then turn errors off and keep scrolling.
- 5
Hint: In All products, try to click Contact or Imprint in the footer.
- 6
Hint: Click All products, then press Tab repeatedly through the products until more load. Where does the next Tab take you?
How to test it
Checklist
Infinite scroll test checklist
0 of 14 checked
Example test cases
TC-INF-001Next page loads at the end of the listpositivehigh
Preconditions
- 250 products, 12 per page
- Network: Fast 4G
Steps
- Open All products
- Scroll to the end of the first 12 products
- Wait for loading to finish
Expected result
One request with the next cursor is sent. Products 13–24 appear below 1–12, with no duplicates and no gap.
TC-INF-002Fast scrolling sends one request per pageedgehigh
Preconditions
- Network throttled to "3G"
Steps
- Fling the list to the bottom
- Keep scrolling while "Loading more" is visible
- Check the network log
Expected result
Only one request per cursor value. Every product appears once.
TC-INF-003End of listedgemedium
Preconditions
- Sale category has 7 products
Steps
- Open Sale
- Scroll to the bottom
- Wait 10 seconds
Expected result
No loading row is shown. An end message says all 7 items are shown. No further requests are sent.
TC-INF-004Error while loading the next pagenegativehigh
Preconditions
- Proxy returns HTTP 500 for the second page request
Steps
- Scroll to the end of page 1
- Wait for the error
- Remove the proxy rule
- Tap Retry
Expected result
Page 1 stays visible. An inline error with Retry appears at the bottom. Retry loads page 2 once; no duplicates.
TC-INF-005Back restores positionpositivehigh
Steps
- Scroll until about 60 products are loaded
- Open product #50
- Go back (browser Back, Back button, or system back gesture)
Expected result
The list shows all 60 loaded products, scrolled so product #50 is where it was.
TC-INF-006Footer links are reachablepositivemedium
Steps
- Open All products
- Try to open Contact, Returns and Imprint
Expected result
Each link can be reached and opened without loading the entire catalog (e.g. via a Load more button after a few pages, or links in the header or menu).
TC-INF-007Keyboard focus after loadingpositivehigh
Preconditions
- Desktop browser, keyboard only
Steps
- Tab through the products until the next page loads
- Press Tab once more
Expected result
Focus moves to the next product (the first newly loaded one), not back to the top of the page.
TC-INF-008Large screen fills itselfedgemedium
Preconditions
- Browser window 2560×1440, or a tablet in landscape
- Page size 10
Steps
- Open the list
- Do not scroll
Expected result
The app keeps loading pages until the screen is full or the data ends, so the user can scroll.
Tip: click a test case to expand it.
Tools
- Network log: DevTools › Network on the web, a proxy tool like Charles or Proxyman for apps. Filter by the list’s API path and compare the cursor or page parameter of every request. Two identical requests are proof of the duplicate bug.
- Throttle: network throttlingNetwork throttlingDeliberately slowing the connection to test how an app behaves on poor networks.Open in glossary → (“3G” in Chrome DevTools, Network Link Conditioner on iOS, emulator settings on Android) makes loading rows, double requests and race conditions visible.
- Break a single page: use a proxy rule or proxy breakpointProxy breakpointPausing a request or response in a proxy tool so you can edit it before it continues.Open in glossary → to fail only the second or third page request, then check the error row and Retry.
- Force the end: filter or search for something with very few results, or use a test account with little data.
- State restoration: on Android, Developer options › Don’t keep activities; on iOS, put the app in the background and stop it from Xcode, then reopen. Rotate mid-scroll.
- Performance: scroll for several minutes. Watch memory in Xcode’s Debug navigator, Android Studio’s Profiler or DevTools’ Performance monitor.
Accessibility
Infinite scroll is one of the hardest patterns to make accessible. Check these:
- Keyboard focus stays put. When items are appended, focus must stay on the item the user was on. Re-rendering the whole list or moving focus to the top sends keyboardKeyboard navigationUsing a website with only the keyboard: Tab to move, Enter or Space to activate, Esc to close.Open in glossary → users back to the start (WCAG 2.4.3 Focus Order, A). See also focus orderFocus orderThe order in which the Tab key moves through interactive elements.Open in glossary →.
- Offer “Load more”. A visible button lets keyboard, switch and screen reader users decide when to load, and lets them reach what’s below the list. Many sites load the first pages automatically, then show the button.
- Announce new items. A polite live region (“12 more products loaded”) tells screen readerScreen readerSoftware that reads the screen aloud for blind and low-vision users, like VoiceOver or TalkBack.Open in glossary → users something changed without moving focus (WCAG 4.1.3 Status Messages, AA). Announce the end and errors too.
- Give it structure. A feed or a plain list with a heading and a result count.
Dev note
On the web, the ARIA feed pattern fits:role="feed"on the container, each item anarticlewitharia-posinsetandaria-setsize(-1if the total is unknown), andaria-busy="true"on the feed while it loads. A plain list (ul/li) with a heading and a result count also works. - The footer and skip links. Keyboard users must reach everything after the list. A “Skip to footer” link or footer links in the header avoid an endless Tab journey.
- VoiceOver and TalkBack swipe through items one by one. Check that new items are reached naturally when swiping past the last one, and that the loading row and errors are read out.
- Virtualized lists can confuse screen readers (“item 5 of 12” when there are 400). Check positions and counts are announced correctly.
- Touch targets for Retry and Load more: at least 44×44 pt on iOS and 48×48 dp on Android. On the web, WCAG 2.5.8 (AA) asks for at least 24×24 CSS px; 44×44 px (2.5.5, AAA) is the comfortable target.
Mobile vs web
Dev note
collectionView(_:willDisplay:forItemAt:) or a prefetch data source; SwiftUI apps often use .onAppear on the last row of a List or LazyVStack.- Pushing an item on a navigation stack keeps the list in memory, so Back usually works. Test what happens after the app is killed in the background, after a memory warning and on a tab switch.
- Tapping the status bar scrolls to the top: does the list stay loaded, and does it keep loading correctly afterwards?
- Test VoiceOver: swiping past the last item should reach newly loaded ones.
- The loading row, error row and end message: test all three.
Dev note
Most apps use the Paging library withRecyclerViewor ComposeLazyColumn. Its load states (append loading, error, end of pagination) map directly to them. - Rotation and process death (Developer options › Don’t keep activities) are the classic causes of lost positions and reloads from page 1.
- Fling hard through hundreds of items: watch for duplicates, placeholders that never fill and jumps.
- Use airplane modeAirplane modeA device setting that turns off all wireless connections at once.Open in glossary → mid-scroll: an error row with Retry, not a frozen spinner.
- Resize the window: very tall windows and zoomed-out pages expose the “never fills the screen” bug.
Dev note
Loading is usually triggered by anIntersectionObserveron a sentinel. - Back, Forward and Refresh: the browser restores scroll for normal pages (and the back/forward cache can restore the whole page), but single-page apps often rebuild the list and must restore it themselves. Check the URL changes as you load more (
?page=or a cursor) if the product needs deep links. - Try Find in page (Ctrl/Cmd+F) for an item you’ve already scrolled past: virtualized lists may not find it.
- Check the footer, the keyboard (Tab and Shift+Tab around a load) and a screen reader (NVDA, VoiceOver) on desktop.
Check your understanding
Quick quiz
0/6 answered · 0 correct
1.What usually triggers loading the next page in infinite scroll?
2.After scrolling fast, products 13–24 appear twice. The network log shows two requests with cursor=a12. What is missing?
3.The server answers with next: null, but "Loading more…" keeps spinning. What should happen?
4.Loading the next page fails. Which behavior is best?
5.Why is a footer with Contact and Imprint a problem on an infinite list?
6.A keyboard user tabs to product 24, more products load, and the next Tab goes to product 1. Which WCAG criterion is affected?
Key takeaways
- Infinite scroll is pagination triggered by scrolling: a sentinel, a prefetch distance, a page size and usually a cursor.
- Test the three bottoms: a small loading row, an inline error with Retry, and a clear end message.
- One request per page: scroll fast and watch the network log for duplicate cursors.
- Back must restore the loaded items and the position; check rotation, process death and refresh too.
- Keep the footer reachable, keep keyboard focus in place, offer Load more, and announce new items.
- Long lists need virtualization or native recycling; test long sessions for speed and memory.
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 ⚡