Introduction
I found a way to avoid the well-known problem where “infinite scroll resets after a browser back,” using a modal that combines Next.js App Router’s Intercepting Routes and Parallel Routes.
Even after using the browser back action, the scroll position of the infinite list is preserved.
You can try it yourself with this demo app.
This is a Next.js framework–specific approach, but I hope it helps anyone who is facing—or has faced—this problem.
Notes
- This post does not explain the low-level details of Intercepting Routes or Parallel Routes. The main goal is to convey how the app feels when infinite scroll is combined with a URL-addressable modal built with Intercepting + Parallel Routes, via the demo app.
- The context is a web application, not a native app.
The Traditional Problem—and Ways Around It
The problem
Infinite scroll UIs have become common, partly due to their affinity with smartphones. They’re often used on apps that consist of a list page and multiple detail pages, like the demo app. (The discussion below assumes this kind of structure.)
However, infinite scroll is known to play poorly with the browser’s back button. You sometimes see apps where you scroll a long way down the list, open a detail page, and then lose your place on back navigation.
This happens because content loaded by infinite scroll is fetched and held on the client; that state is lost on navigation. Browsers remember URL history and scroll positions, but the DOM and data added dynamically by JavaScript are (by default) not restored on the next page load.
On smartphones, swipe-back is effortless, which leaves UI design caught between competing demands.
Workarounds
Consider pagination
First things first: if you fall back to traditional pagination, this browser-back problem won’t occur.
Do you really need infinite scroll? That’s a big topic, but resources like the following are a good starting point for re-evaluation:
Infinite Scrolling: When to Use It, When to Avoid It
Use bfcache
bfcache caches the entire page (DOM, JavaScript execution state, etc.) in memory and restores it instantly on browser back. A big advantage is that it doesn’t depend on your app’s implementation. For improving an existing app, it’s a strong candidate—though there are challenges like complex eligibility rules.
This is outside the scope of this post (and I don’t have deep expertise here), so please see articles like the one below:
Back/forward cache helped Yahoo! JAPAN News increase revenue by 9% on mobile
Show details in a modal, new tab, or new window
Another known workaround is to show detail content from the list without a page navigation—via a modal, new tab, or new window.
Opening the same app in a separate tab/window is only the best choice in limited cases. By contrast, there are many cases where a modal makes sense. Modals keep the user in context while focusing attention on specific content, which fits the pattern of briefly showing details while browsing a list.
However, traditional modals toggle display purely from client state, so the URL remains the list page. That leads to:
- Pressing browser back while a modal is open moves to the URL before the list page, not just “close the modal.” While this is natural for a traditional modal, it can violate user expectations and often causes accidental app exits.
- You cannot share a detail page URL: recipients of a “modal link” just land on the list page.
- Search engines can’t index detail pages: without unique URLs, you lose long-tail organic traffic from detail content.
If you adopt modal display in the traditional way, you accept these drawbacks.
Solving It with Next.js Intercepting Routes + Parallel Routes Modal
Next.js App Router provides Intercepting Routes and Parallel Routes. As the official docs show, combining them lets you create a URL-addressable modal.
When navigating within the app (soft navigation using the <Link> component, etc.), the detail page is shown as a modal; on direct access (hard navigation), it is rendered as a regular page. This works by rendering multiple routes in parallel (Parallel Routes) and letting Intercepting Routes swap which one is rendered based on navigation context.
As shown in the demo GIF, the detail content appears as a modal because the user navigated within the app. But if you access the detail page directly, the modal does not appear:

Display when directly accessing a detail page
Here’s how this differs from traditional “detail modals”:
- Browser back closes the modal—and even with infinite scroll, you return to your exact position in the list. This is the main topic of this post. Conversely, a forward action re-opens the modal, which is especially handy on iOS.
- You can share the detail URL: while the modal is open, the URL switches to the detail path, enabling sharing and bookmarking.
- Search engines can index details: you can expect long-tail organic traffic from many detail pages.
How the demo app is put together
In the demo, the product list page injects a detail modal using this structure:
app/
├── (content)/
│ ├── @modal/ # (a) - Parallel Routes slot
│ │ ├── (.)items/[id]/ # (b) - Intercepting Route that intercepts the sibling (f) on soft navigation
│ │ │ └── page.tsx # (c) - Detail page for soft navigation (modal)
│ │ │── default.tsx # (d) - Returns null when the path doesn't match
│ │ └── layout.tsx # (e) - Modal-specific layout and styles applied under @modal
│ ├── items/[id]/page.tsx # (f) - Canonical detail page on direct access
│ ├── page.tsx # (g) - List page
│ ├── layout.tsx # (h) - Renders @modal in parallel in addition to the normal layout (see below)
│ ├── ...
├── ...
layout.tsx (h)
export default function ContentLayout({
children,
modal,
}: {
children: React.ReactNode;
modal: React.ReactNode;
}) {
return (
<>
<Header />
{children}
{modal}
<Footer />
</>
);
}
You can browse the real source on GitHub.
Verifying the Behavior in the Demo App
Use the demo app to confirm everything above. It’s a responsive app with a typical structure: one list page and multiple detail pages.
A typical user flow: open details from the list page
Here’s a short video that demonstrates the interaction:
A brief demo of opening details as a URL-addressable modal and returning with the list’s scroll position preserved.
- Go to the list page.
- Scroll to the bottom to trigger infinite loading.
- Tap (click) a product card.
- The modal detail page
(c)appears. (The URL becomes/items/[id].) - From “Related Products,” tap (click) another product card.
- The modal detail page
(c)appears again. (The URL is/items/[id].) - Press browser back to return to the previous product’s modal.
- Press browser back again to close the modal and return to the exact position on the list.
Key points:
- To keep the user aware that they’re “still browsing the list,” the modal is a conventional semi-transparent overlay. If you want it to feel more like a full page transition, you can use a full-screen modal instead (implement that in
(e) layout.tsx). - Because the list and modal are rendered in parallel (Parallel Routes), adding an item to the cart from the modal immediately reflects on the list. (State management uses Zustand.)
- Pressing a category tag navigates to the category list via a hard navigation (
<a>tag). At that point, we consider the user’s focus to have shifted from “Home (/) list” to “a specific category list.” In real apps, you’ll need to draw this line somewhere.
Accessing a shared link to a detail page
- Directly access a detail page like this with a
/items/[id]URL. - The canonical (non-modal) detail page
(f)is shown.
Conclusion
As far as I could find, there aren’t yet many discussions of an Intercepting + Parallel Routes modal specifically from the standpoint of compatibility with infinite scroll, so I wrote this up. The technology is still evolving, and I haven’t run this in production yet, so I expect there will be challenges—but personally, the interaction feels quite good.
I hope this helps Next.js users and anyone evaluating frameworks who isn’t yet familiar with Next.js.
Stack
The demo app uses:
- Next.js 15 (App Router)
- React 19
- TypeScript 5
- Tailwind CSS 4
- shadcn/ui (UI component library)
- Lucide React (icon library)
- Zustand (cart state management)
- Intersection Observer API (infinite scroll implementation)
I built most of it with Claude Code. While the demo works for its purpose here, parts outside the scope of this post should be treated as reference only.
github.com/derarion/nextjs-infinite-scroll-app-demo

Top comments (0)