<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: derarion</title>
    <description>The latest articles on DEV Community by derarion (@derarion).</description>
    <link>https://dev.to/derarion</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1140258%2Fcb32c4ac-4ff7-47fc-a14e-353c93f8cbc2.png</url>
      <title>DEV Community: derarion</title>
      <link>https://dev.to/derarion</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/derarion"/>
    <language>en</language>
    <item>
      <title>Solving “Browser Back Resets Infinite Scroll” with a Next.js URL-Addressable Modal</title>
      <dc:creator>derarion</dc:creator>
      <pubDate>Thu, 23 Oct 2025 17:12:57 +0000</pubDate>
      <link>https://dev.to/derarion/solving-browser-back-resets-infinite-scroll-with-a-nextjs-url-addressable-modal-1doa</link>
      <guid>https://dev.to/derarion/solving-browser-back-resets-infinite-scroll-with-a-nextjs-url-addressable-modal-1doa</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;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 &lt;a href="https://nextjs.org/docs/app/api-reference/file-conventions/intercepting-routes" rel="noopener noreferrer"&gt;Intercepting Routes&lt;/a&gt; and &lt;a href="https://nextjs.org/docs/app/api-reference/file-conventions/parallel-routes" rel="noopener noreferrer"&gt;Parallel Routes&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw0kcl6pg5ufjl1u7muls.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw0kcl6pg5ufjl1u7muls.gif" alt="Demo GIF showing that returning from a detail modal preserves the list’s scroll position"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Even after using the browser back action, the scroll position of the infinite list is preserved.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You can try it yourself with this &lt;a href="https://nextjs-infinite-scroll-app-demo.vercel.app/" rel="noopener noreferrer"&gt;demo app&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This is a &lt;strong&gt;Next.js framework–specific approach&lt;/strong&gt;, but I hope it helps anyone who is facing—or has faced—this problem.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Notes&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;This post does not explain the low-level details of Intercepting Routes or Parallel Routes. The main goal is to convey how the app &lt;em&gt;feels&lt;/em&gt; when infinite scroll is combined with a URL-addressable modal built with Intercepting + Parallel Routes, via the demo app.&lt;/li&gt;
&lt;li&gt;The context is a web application, not a native app.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Traditional Problem—and Ways Around It
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The problem
&lt;/h3&gt;

&lt;p&gt;Infinite scroll UIs have become common, partly due to their affinity with smartphones. They’re often used on &lt;strong&gt;apps that consist of a list page and multiple detail pages&lt;/strong&gt;, like the &lt;a href="https://nextjs-infinite-scroll-app-demo.vercel.app/" rel="noopener noreferrer"&gt;demo app&lt;/a&gt;. (The discussion below assumes this kind of structure.)&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;On smartphones, swipe-back is effortless, which leaves UI design caught between competing demands.&lt;/p&gt;

&lt;h3&gt;
  
  
  Workarounds
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Consider pagination
&lt;/h4&gt;

&lt;p&gt;First things first: if you fall back to traditional pagination, this browser-back problem won’t occur.&lt;/p&gt;

&lt;p&gt;Do you &lt;em&gt;really&lt;/em&gt; need infinite scroll? That’s a big topic, but resources like the following are a good starting point for re-evaluation:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.nngroup.com/articles/infinite-scrolling-tips/" rel="noopener noreferrer"&gt;Infinite Scrolling: When to Use It, When to Avoid It&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Use bfcache
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Glossary/bfcache" rel="noopener noreferrer"&gt;bfcache&lt;/a&gt; caches the entire page (DOM, JavaScript execution state, etc.) in memory and restores it instantly on browser back. A big advantage is that it &lt;strong&gt;doesn’t depend on your app’s implementation&lt;/strong&gt;. For improving an existing app, it’s a strong candidate—though there are challenges like complex eligibility rules.&lt;/p&gt;

&lt;p&gt;This is outside the scope of this post (and I don’t have deep expertise here), so please see articles like the one below:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://web.dev/case-studies/yahoo-japan-news-bfcache" rel="noopener noreferrer"&gt;Back/forward cache helped Yahoo! JAPAN News increase revenue by 9% on mobile&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Show details in a modal, new tab, or new window
&lt;/h4&gt;

&lt;p&gt;Another known workaround is to show detail content from the list &lt;em&gt;without&lt;/em&gt; a page navigation—via a modal, new tab, or new window.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;However, traditional modals toggle display purely from client state, so the URL remains the list page. That leads to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pressing browser back while a modal is open moves to the URL &lt;em&gt;before&lt;/em&gt; the list page&lt;/strong&gt;, not just “close the modal.” While this is natural for a traditional modal, it can violate user expectations and often causes accidental app exits.&lt;/li&gt;
&lt;li&gt;You cannot share a detail page URL: recipients of a “modal link” just land on the list page.&lt;/li&gt;
&lt;li&gt;Search engines can’t index detail pages: without unique URLs, you lose long-tail organic traffic from detail content.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you adopt modal display in the traditional way, you accept these drawbacks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solving It with Next.js Intercepting Routes + Parallel Routes Modal
&lt;/h2&gt;

&lt;p&gt;Next.js App Router provides &lt;a href="https://nextjs.org/docs/app/api-reference/file-conventions/intercepting-routes" rel="noopener noreferrer"&gt;Intercepting Routes&lt;/a&gt; and &lt;a href="https://nextjs.org/docs/app/api-reference/file-conventions/parallel-routes" rel="noopener noreferrer"&gt;Parallel Routes&lt;/a&gt;. As the &lt;a href="https://nextjs.org/docs/app/api-reference/file-conventions/intercepting-routes#modals" rel="noopener noreferrer"&gt;official docs&lt;/a&gt; show, combining them lets you create a &lt;strong&gt;URL-addressable modal&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When navigating &lt;em&gt;within the app&lt;/em&gt; (soft navigation using the &lt;a href="https://nextjs.org/docs/app/api-reference/components/link" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;Link&amp;gt;&lt;/code&gt;&lt;/a&gt; component, etc.), the detail page is shown as a modal; on &lt;em&gt;direct access&lt;/em&gt; (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.&lt;/p&gt;

&lt;p&gt;As shown in the demo GIF, the detail content appears as a modal because the user navigated within the app. But if you &lt;a href="https://nextjs-infinite-scroll-app-demo.vercel.app/items/100" rel="noopener noreferrer"&gt;access the detail page directly&lt;/a&gt;, the modal does not appear:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fibf20emoxl5mnrumnatv.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fibf20emoxl5mnrumnatv.webp" alt="Direct-access detail page rendered without a modal overlay"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Display when directly accessing a detail page&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Here’s how this differs from traditional “detail modals”:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Browser back closes the modal—and even with infinite scroll, you return to your exact position in the list.&lt;/strong&gt; This is the main topic of this post. Conversely, a forward action re-opens the modal, which is especially handy on iOS.&lt;/li&gt;
&lt;li&gt;You can share the detail URL: while the modal is open, the URL switches to the detail path, enabling sharing and bookmarking.&lt;/li&gt;
&lt;li&gt;Search engines can index details: you can expect long-tail organic traffic from many detail pages.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  How the demo app is put together
&lt;/h3&gt;

&lt;p&gt;In the demo, the product list page injects a detail modal using this structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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)
│   ├── ...
├── ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;layout.tsx (h)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ContentLayout&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;modal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ReactNode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;modal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ReactNode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Header&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;modal&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Footer&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can browse the real source on &lt;a href="https://github.com/derarion/nextjs-infinite-scroll-app-demo" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verifying the Behavior in the Demo App
&lt;/h2&gt;

&lt;p&gt;Use the &lt;a href="https://nextjs-infinite-scroll-app-demo.vercel.app/" rel="noopener noreferrer"&gt;demo app&lt;/a&gt; to confirm everything above. It’s a responsive app with a typical structure: one list page and multiple detail pages.&lt;/p&gt;

&lt;h3&gt;
  
  
  A typical user flow: open details from the list page
&lt;/h3&gt;

&lt;p&gt;Here’s a short video that demonstrates the interaction:&lt;/p&gt;

&lt;p&gt;

  &lt;iframe src="https://www.youtube.com/embed/ThUt6rZZlM8"&gt;
  &lt;/iframe&gt;


&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A brief demo of opening details as a URL-addressable modal and returning with the list’s scroll position preserved.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Go to the &lt;a href="https://nextjs-infinite-scroll-app-demo.vercel.app/" rel="noopener noreferrer"&gt;list page&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Scroll to the bottom to trigger infinite loading.&lt;/li&gt;
&lt;li&gt;Tap (click) a product card.&lt;/li&gt;
&lt;li&gt;The modal detail page &lt;code&gt;(c)&lt;/code&gt; appears. (The URL becomes &lt;code&gt;/items/[id]&lt;/code&gt;.)&lt;/li&gt;
&lt;li&gt;From “Related Products,” tap (click) another product card.&lt;/li&gt;
&lt;li&gt;The modal detail page &lt;code&gt;(c)&lt;/code&gt; appears again. (The URL is &lt;code&gt;/items/[id]&lt;/code&gt;.)&lt;/li&gt;
&lt;li&gt;Press browser back to return to the previous product’s modal.&lt;/li&gt;
&lt;li&gt;Press browser back again to close the modal and return to the exact position on the list.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Key points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;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 &lt;code&gt;(e) layout.tsx&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;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 &lt;a href="https://zustand-demo.pmnd.rs/" rel="noopener noreferrer"&gt;Zustand&lt;/a&gt;.)&lt;/li&gt;
&lt;li&gt;Pressing a category tag navigates to the category list via a hard navigation (&lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; tag). At that point, we consider the user’s focus to have shifted from “Home (&lt;code&gt;/&lt;/code&gt;) list” to “a specific category list.” In real apps, you’ll need to draw this line somewhere.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Accessing a shared link to a detail page
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Directly access a detail page &lt;a href="https://nextjs-infinite-scroll-app-demo.vercel.app/items/100" rel="noopener noreferrer"&gt;like this&lt;/a&gt; with a &lt;code&gt;/items/[id]&lt;/code&gt; URL.&lt;/li&gt;
&lt;li&gt;The canonical (non-modal) detail page &lt;code&gt;(f)&lt;/code&gt; is shown.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;As far as I could find, there aren’t yet many discussions of an Intercepting + Parallel Routes modal specifically from the standpoint of &lt;em&gt;compatibility with infinite scroll&lt;/em&gt;, 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.&lt;/p&gt;

&lt;p&gt;I hope this helps Next.js users and anyone evaluating frameworks who isn’t yet familiar with Next.js.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stack
&lt;/h3&gt;

&lt;p&gt;The demo app uses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Next.js 15 (App Router)&lt;/li&gt;
&lt;li&gt;React 19&lt;/li&gt;
&lt;li&gt;TypeScript 5&lt;/li&gt;
&lt;li&gt;Tailwind CSS 4&lt;/li&gt;
&lt;li&gt;shadcn/ui (UI component library)&lt;/li&gt;
&lt;li&gt;Lucide React (icon library)&lt;/li&gt;
&lt;li&gt;Zustand (cart state management)&lt;/li&gt;
&lt;li&gt;Intersection Observer API (infinite scroll implementation)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;br&gt;&lt;br&gt;
&lt;a href="https://github.com/derarion/nextjs-infinite-scroll-app-demo" rel="noopener noreferrer"&gt;github.com/derarion/nextjs-infinite-scroll-app-demo&lt;/a&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>webdev</category>
      <category>ui</category>
      <category>frontend</category>
    </item>
  </channel>
</rss>
