Infinite Scroll
Load the next page as the user nears the end of the list.
How it works
Infinite scroll loads the next page of a long list as the reader nears its end, so they can keep reading without clicking through pages. The hard parts hide behind that simple idea: never firing two requests at once, never showing a post twice or skipping one when new posts arrive, and never moving the text someone is reading.
- Observe, don't listen: an
IntersectionObserverwatches a marker after the last post; no scroll handler decides when to load. - One request at a time: a ref blocks a second request before React state could.
- Cursors, not page numbers: new posts never shift older pages, so nothing is duplicated or skipped.
- Content never moves on its own: pages only append below; new posts wait behind a pill until you ask.
Read moreShow less
Where you'd use it
- Social and news feeds, where people browse rather than look for one item.
- Activity logs and notification histories that grow over time.
- Image or product grids for casual browsing, paired with a “Load more” fallback.
- Search results that are skimmed in order, when there's no footer people need to reach.
How we built it
The feed scrolls inside its own fixed-height box. An IntersectionObserver uses that box as its root and the trigger distance as a bottom rootMargin, so it fires when the end marker comes within that distance. A ref is checked and set synchronously before every request, because React state updates too late to stop a second trigger. The observer is re-created each time loading settles, so short pages keep loading until the box can scroll.
The server returns an opaque “older than post #N” cursor instead of a page number. New posts get higher numbers, so they never shift older pages. A timer checks for newer posts in the background (skipped while the tab is hidden); they wait behind a pill and go to the top only when you press it.
Posts, cursor and the first visible post with its pixel offset are kept in module memory. Coming back to the practice restores them in a layout effect before paint, with no new requests. Leaving aborts any request still in flight.
Best practices
Do: Use an
IntersectionObserverwith arootMarginso the next page starts loading before the end.Avoid: Measuring positions in a scroll handler on every scroll event.
Do: Guard requests with a ref that is set synchronously, and abort them when the reader leaves.
Avoid: Relying on a loading state alone: fast scrolling fires the same page twice.
Do: Page with a cursor from the server and drop any post already in the list.
Avoid: Offset or page-number paging on data that changes: inserts cause duplicates and gaps.
Do: Mark the list as a
role="feed"of focusablearticles witharia-posinset/aria-setsize, announce progress politely, and keep a visible Load more button.Avoid: A feed that only grows by scrolling a mouse wheel, with nothing announced.
Do: Append below and hold new posts until the reader asks for them.
Avoid: Inserting content above the reader, which pushes what they are reading out of view.
Try it with the demo controls
- Latency
- Slows each page request. Scroll fast: the stats line still shows one request per page and a single “Loading more posts…” row.
- Failure rate
- Page requests fail at random. Automatic loading pauses on an error and waits for you to press Retry.
- Page size
- Applies from the next request, mid-feed too: the list continues with no post repeated or skipped.
- Trigger distance
- How early the next page loads. At 0 px it waits until the very end; at 1200 px it loads well ahead.
- Check every
- How often the feed checks the server for newer posts, from 5 to 60 seconds.
- Simulate new posts
- Creates 3 posts on the server. After the next check, a pill appears and the list doesn't move.
- New posts pill
- Shows the count (capped at “99+”). Pressing it inserts the posts at the top and scrolls there.
- Load more / Retry
- Loads the next page by hand, or after an error. At the end it becomes “You've reached the end · N posts”.
- Back to top
- Appears once you scroll more than two box heights down; it returns to the top and focuses the first post.
- Page Down / Page Up
- With a post focused, moves focus to the next or previous post.
- Reset feed
- Clears the saved feed and position, then loads the first page again.
Trade-offs and limits
- Every loaded post stays in the page: no windowing, so very long sessions grow the DOM (the virtualized list practice covers that).
- Loads only downward; two-way scrolling and real-time push belong to the chat practice. New posts arrive by polling.
- The feed and position survive in-app navigation but not a full reload, and demo control values reset when you leave.
- After a failed page, automatic loading waits for Retry instead of retrying with backoff.
Demo controls
Requests: 1 · Posts loaded: 0 · Total on server: ?