[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"article-nextjs-weekly-parallel-intercepting-routes-modals":44,"search-suggestions":428,"related-nextjs-weekly-parallel-intercepting-routes-modals":465,"code:tsx:true:kanrmk":511,"code:plain:true:1rtqf98":512,"code:tsx:true:12378a1":513,"code:tsx:true:1yf2xod":514,"code:plain:true:9jqsnj":515,"code:tsx:true:9pvbjq":516,"code:tsx:true:23brho":517,"code:tsx:true:7fo6vg":518,"code:tsx:true:zj4zka":519,"comments-01a09181-2f3b-7349-b651-fd9cdda0b931":520},[4,20,32],{"id":5,"slug":6,"name":7,"tagline":8,"description":9,"accentFrom":10,"accentTo":11,"icon":12,"defaultLocale":13,"locales":14,"features":16,"position":19},"019fe637-3d33-714b-b57f-23e163ffca0c","dev","Web Development","Read it. Run it. Prove it.","A post a day on modern web development — most with an editable playground and a quiz that explains every answer. Free, no account needed.","violet-500","cyan-400","◇","en",[13,15],"fa",{"courses":17,"paths":17,"articles":17,"exams":18,"flashcards":18,"packages":17,"community":17,"certificates":17,"teams":17,"commerce":17},true,false,0,{"id":21,"slug":22,"name":23,"tagline":24,"description":25,"accentFrom":26,"accentTo":10,"icon":27,"defaultLocale":13,"locales":28,"features":30,"position":31},"019fe637-3dc2-754c-8657-0f175bfee7c6","lang","Languages","Learn a language the way you learn a codebase.","Grammar explained the way good documentation explains an API — one idea at a time, each with a quiz.","amber-400","⌘",[13,15,29],"es",{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":17,"packages":18,"community":17,"certificates":17,"teams":18,"commerce":18},2,{"id":33,"slug":34,"name":35,"tagline":36,"description":37,"accentFrom":38,"accentTo":39,"icon":40,"defaultLocale":13,"locales":41,"features":42,"position":43},"7b3c16f2-931d-410e-802e-e1fa4edab7de","soft","Soft Skills","The half of the job nobody wrote documentation for.","Weekly, on the parts of working life that decide more than your code does — first weeks, meetings, interviews, promotions, and the people around you. Written from what actually happens, and recorded as a podcast you can listen to on the walk.","emerald-400","teal-300","◉",[13],{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":18,"packages":18,"community":17,"certificates":18,"teams":18,"commerce":18},3,{"id":45,"slug":46,"title":47,"subtitle":48,"excerpt":49,"coverUrl":50,"locale":13,"readingMinutes":51,"publishedAt":52,"viewCount":53,"likeCount":19,"commentCount":19,"author":54,"vertical":59,"topic":60,"tags":63,"_count":74,"playground":76,"body":78,"bodyMd":399,"seo":400,"translationGroupId":402,"series":403,"podcastUrl":48,"verticalId":5,"thread":416,"assessments":418,"translations":424,"quiz":426},"01a09181-2f3b-7349-b651-fd9cdda0b931","nextjs-weekly-parallel-intercepting-routes-modals","Next.js Parallel & Intercepting Routes: Modals Done Right",null,"How Next.js parallel routes (@slot) and intercepting routes ((.), (..), (...)) combine to build shareable, refreshable modals — verified against Next.js 16.3.","\u002Fmedia\u002Fcovers\u002Fnextjs-weekly-parallel-intercepting-routes-modals.png",14,"2026-09-15T11:26:13.087Z",27,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},"nextjs","Nextjs",[64,65,68,71],{"slug":61,"name":62,"color":48},{"slug":66,"name":67,"color":48},"react","React",{"slug":69,"name":70,"color":48},"webdev","Webdev",{"slug":72,"name":73,"color":48},"tutorial","Tutorial",{"assessments":75},1,{"slug":46,"title":77},"Next.js Parallel &amp; Intercepting Routes — interactive playground",{"blocks":79,"version":75},[80,84,87,92,95,104,107,110,113,127,131,134,140,143,149,152,155,158,161,164,167,170,173,178,182,185,188,191,195,198,201,204,208,211,215,219,222,225,228,231,235,238,241,244,247,250,253,256,263,266,273,276,279,282,286,289,292,295,298,301,304,307,310,346,350,353,356,359,362,365,368,375,378,381,384,387,390,393],{"id":81,"html":82,"type":83},"b1","\u003Cp>You build a photo grid. Clicking a thumbnail should pop up a modal with the full photo — the feed stays visible and scrolled to where the user left it. \u003Ccode>useState\u003C\u002Fcode> and a \u003Ccode>{open &amp;&amp; &lt;PhotoModal \u002F&gt;}\u003C\u002Fcode> conditional get this working in about ten minutes. Then someone refreshes the page while the modal is open, and the photo is just gone — back to the bare feed, because that boolean lived in memory and the URL never knew a modal was open. Someone else shares the link expecting to send a specific photo, and it opens to... the feed. The modal was never a place; it was a client state flag.\u003C\u002Fp>","paragraph",{"id":85,"html":86,"type":83},"b2","\u003Cp>This is one of the few UI problems the App Router&#39;s own routing model was built to solve, and it does it with two conventions that are easy to skim past in the docs and hard to use correctly from memory: \u003Cstrong>parallel routes\u003C\u002Fstrong> and \u003Cstrong>intercepting routes\u003C\u002Fstrong>. This article is written against \u003Cstrong>Next.js 16.3\u003C\u002Fstrong> (the current Active LTS release, verified against the framework&#39;s own file-convention docs and its GitHub releases in September 2026); the conventions below have been stable since Next.js 13 and are not part of the newer Cache Components model, so nothing here changes if you&#39;re on an app that hasn&#39;t adopted \u003Ccode>cacheComponents\u003C\u002Fcode> yet.\u003C\u002Fp>",{"id":88,"html":89,"text":90,"type":91,"level":31},"b3","What you&#39;ll learn","What you'll learn","heading",{"id":93,"html":94,"type":83},"b4","\u003Cp>By the end of this article you&#39;ll be able to:\u003C\u002Fp>",{"id":96,"type":97,"items":98,"ordered":18},"b5","list",[99,100,101,102,103],"Explain what a parallel route slot (\u003Ccode>@slot\u003C\u002Fcode>) actually is, and why it doesn&#39;t add a segment to the URL","Use \u003Ccode>default.tsx\u003C\u002Fcode> correctly, and explain exactly when Next.js needs it and why its absence produces a 404","Read the \u003Ccode>(.)\u003C\u002Fcode>, \u003Ccode>(..)\u003C\u002Fcode>, \u003Ccode>(..)(..)\u003C\u002Fcode>, and \u003Ccode>(...)\u003C\u002Fcode> intercepting-route matchers and know which one a given folder move needs","Combine both conventions to build a modal that has a real, shareable, refreshable URL","Recognize the difference between a client-side navigation into an intercepted route and a hard navigation to the same URL, and why they render different things on purpose",{"id":105,"html":106,"text":106,"type":91,"level":31},"b6","Who this is for",{"id":108,"html":109,"type":83},"b7","\u003Cp>You&#39;ve built at least a small App Router project — you know what \u003Ccode>page.tsx\u003C\u002Fcode> and \u003Ccode>layout.tsx\u003C\u002Fcode> do, and you&#39;ve used \u003Ccode>&lt;Link&gt;\u003C\u002Fcode> for client-side navigation. You don&#39;t need any prior experience with parallel or intercepting routes; we build both from nothing.\u003C\u002Fp>",{"id":111,"html":112,"text":112,"type":91,"level":31},"b8","Table of contents",{"id":114,"type":97,"items":115,"ordered":18},"b9",[116,117,118,119,120,121,122,123,124,125,126],"\u003Ca href=\"#the-problem-a-modal-that-isnt-really-a-place\">The problem: a modal that isn&#39;t really a place\u003C\u002Fa>","\u003Ca href=\"#the-mental-model-slots-and-routes-that-fill-them-differently\">The mental model: slots, and routes that fill them differently\u003C\u002Fa>","\u003Ca href=\"#stage-1-a-parallel-route-slot-on-its-own\">Stage 1: a parallel route slot on its own\u003C\u002Fa>","\u003Ca href=\"#stage-2-defaulttsx-and-the-404-it-prevents\">Stage 2: default.tsx and the 404 it prevents\u003C\u002Fa>","\u003Ca href=\"#stage-3-intercepting-the-photo-route-into-the-slot\">Stage 3: intercepting the photo route into the slot\u003C\u002Fa>","\u003Ca href=\"#stage-4-closing-the-modal\">Stage 4: closing the modal\u003C\u002Fa>","\u003Ca href=\"#edge-cases-and-gotchas\">Edge cases and gotchas\u003C\u002Fa>","\u003Ca href=\"#best-practices\">Best practices\u003C\u002Fa>","\u003Ca href=\"#faq\">FAQ\u003C\u002Fa>","\u003Ca href=\"#cheat-sheet\">Cheat sheet\u003C\u002Fa>","\u003Ca href=\"#key-takeaways\">Key takeaways\u003C\u002Fa>",{"id":128,"html":129,"text":130,"type":91,"level":31},"b10","The problem: a modal that isn&#39;t really a place","The problem: a modal that isn't really a place",{"id":132,"html":133,"type":83},"b11","\u003Cp>Here&#39;s the naive version, and it&#39;s genuinely how most people reach for this first:\u003C\u002Fp>",{"id":135,"code":136,"type":137,"language":138,"highlight":139},"b12","\u002F\u002F app\u002Ffeed\u002Fpage.tsx — the \"wrong way first\"\n\"use client\";\nimport { useState } from \"react\";\n\nexport default function Feed() {\n  const [openPhoto, setOpenPhoto] = useState\u003Cstring | null>(null);\n\n  return (\n    \u003C>\n      \u003CPhotoGrid onSelect={(id) => setOpenPhoto(id)} \u002F>\n      {openPhoto && (\n        \u003CPhotoModal id={openPhoto} onClose={() => setOpenPhoto(null)} \u002F>\n      )}\n    \u003C\u002F>\n  );\n}","code","tsx",[],{"id":141,"html":142,"type":83},"b13","\u003Cp>This works exactly as long as the user never leaves the tab. The moment they do any of the following, it falls apart:\u003C\u002Fp>",{"id":144,"type":97,"items":145,"ordered":18},"b14",[146,147,148],"\u003Cstrong>Refresh the page.\u003C\u002Fstrong> \u003Ccode>openPhoto\u003C\u002Fcode> was never anywhere but React state — it&#39;s gone. The URL is still just \u003Ccode>\u002Ffeed\u003C\u002Fcode>.","\u003Cstrong>Share the link.\u003C\u002Fstrong> There&#39;s nothing to share; the modal was never addressable.","\u003Cstrong>Use the back button.\u003C\u002Fstrong> The browser doesn&#39;t know a modal was &quot;opened&quot; — there&#39;s no history entry for it.",{"id":150,"html":151,"type":83},"b15","\u003Cp>The fix people reach for next is a separate route, \u003Ccode>\u002Fphoto\u002F[id]\u002Fpage.tsx\u003C\u002Fcode>. That solves the URL problem, but now clicking a thumbnail navigates \u003Cem>away\u003C\u002Fem> from the feed entirely — the grid, its scroll position, and any in-flight state are gone, replaced by a page whose whole job is to show one photo. You&#39;ve traded &quot;not a real place&quot; for &quot;a real place that destroys the one you were just looking at.&quot;\u003C\u002Fp>",{"id":153,"html":154,"type":83},"b16","\u003Cp>What you actually want is a route that is real — refreshable, shareable, back-button-able — but that, when reached by clicking a link \u003Cem>from the feed\u003C\u002Fem>, renders as an overlay on top of the feed instead of replacing it. That&#39;s not a state management problem. It&#39;s a routing problem, and Next.js has a routing answer.\u003C\u002Fp>",{"id":156,"html":157,"text":157,"type":91,"level":31},"b17","The mental model: slots, and routes that fill them differently",{"id":159,"html":160,"type":83},"b18","\u003Cp>\u003Cstrong>The mental model:\u003C\u002Fstrong> a layout can have more than one independently-rendered subtree — Next.js calls each one a \u003Cstrong>slot\u003C\u002Fstrong>, written as a folder named \u003Ccode>@slotname\u003C\u002Fcode>. A slot is not a route segment; it doesn&#39;t appear in the URL and doesn&#39;t count as a level of nesting for anything else in the app. It exists purely so a layout can accept several pieces of UI as named &quot;slots&quot; and place them wherever it wants, each one navigable on its own.\u003C\u002Fp>",{"id":162,"html":163,"type":83},"b19","\u003Cp>An \u003Cstrong>intercepting route\u003C\u002Fstrong> is the second, separate piece: a way for one route to say &quot;when the user gets to me by clicking a link from somewhere specific, render \u003Cem>this\u003C\u002Fem> UI instead of the destination&#39;s normal page — but if they land on me any other way (a fresh visit, a refresh, a shared link), render the real thing.&quot; The folder name encodes \u003Cem>how far away\u003C\u002Fem> &quot;somewhere specific&quot; is, using a dot convention measured in route segments: \u003Ccode>(.)\u003C\u002Fcode> the same level, \u003Ccode>(..)\u003C\u002Fcode> one level up, \u003Ccode>(..)(..)\u003C\u002Fcode> two levels up, \u003Ccode>(...)\u003C\u002Fcode> all the way from the app&#39;s root. Because slots aren&#39;t segments, they don&#39;t count when you&#39;re counting dots — this is the detail that trips people up first, and it&#39;s covered below.\u003C\u002Fp>",{"id":165,"html":166,"type":83},"b20","\u003Cp>Put together: the feed&#39;s layout gets a \u003Ccode>@modal\u003C\u002Fcode> slot. Normally that slot renders nothing. A link to \u003Ccode>\u002Fphoto\u002F[id]\u003C\u002Fcode> from inside the feed gets \u003Cem>intercepted\u003C\u002Fem> and rendered into the \u003Ccode>@modal\u003C\u002Fcode> slot as an overlay — same URL, same address bar, same shareable link, but rendered as a modal because of \u003Cem>how\u003C\u002Fem> the user arrived. Land on \u003Ccode>\u002Fphoto\u002F[id]\u003C\u002Fcode> directly, and the intercepting route steps aside; the real, full \u003Ccode>\u002Fphoto\u002F[id]\u002Fpage.tsx\u003C\u002Fcode> renders instead.\u003C\u002Fp>",{"id":168,"html":169,"text":169,"type":91,"level":31},"b21","Stage 1: a parallel route slot on its own",{"id":171,"html":172,"type":83},"b22","\u003Cp>Start with just the slot mechanic, no interception yet. A layout can declare extra props beyond \u003Ccode>children\u003C\u002Fcode> by naming folders \u003Ccode>@something\u003C\u002Fcode>:\u003C\u002Fp>",{"id":174,"code":175,"type":137,"language":176,"highlight":177},"b23","app\u002F\n  dashboard\u002F\n    layout.tsx\n    page.tsx\n    @analytics\u002F\n      page.tsx\n    @team\u002F\n      page.tsx","plain",[],{"id":179,"code":180,"type":137,"language":138,"highlight":181},"b24","\u002F\u002F app\u002Fdashboard\u002Flayout.tsx\nexport default function DashboardLayout({\n  children,\n  analytics,\n  team,\n}: {\n  children: React.ReactNode;\n  analytics: React.ReactNode;\n  team: React.ReactNode;\n}) {\n  return (\n    \u003Cdiv className=\"dashboard-grid\">\n      \u003Cmain>{children}\u003C\u002Fmain>\n      \u003Caside>{analytics}\u003C\u002Faside>\n      \u003Caside>{team}\u003C\u002Faside>\n    \u003C\u002Fdiv>\n  );\n}",[],{"id":183,"html":184,"type":83},"b25","\u003Cp>\u003Cstrong>Key concept:\u003C\u002Fstrong> \u003Ccode>@analytics\u003C\u002Fcode> and \u003Ccode>@team\u003C\u002Fcode> are props on the layout, matched by folder name, not routes a visitor can navigate to directly. Each one is its own subtree with its own \u003Ccode>loading.tsx\u003C\u002Fcode> and \u003Ccode>error.tsx\u003C\u002Fcode> if you want them — the analytics panel can stream in behind its own \u003Ccode>&lt;Suspense&gt;\u003C\u002Fcode> boundary while the team panel is already sitting there rendered, because Next.js renders each slot independently. This alone is useful even with zero interception: it&#39;s how you give one section of a page its own loading and error state without wrapping the whole route in a single boundary.\u003C\u002Fp>",{"id":186,"html":187,"text":187,"type":91,"level":31},"b26","Stage 2: default.tsx and the 404 it prevents",{"id":189,"html":190,"type":83},"b27","\u003Cp>Slots need a \u003Ccode>default.tsx\u003C\u002Fcode> for a specific reason: Next.js has to render \u003Cem>something\u003C\u002Fem> in every slot on every request, and on a hard navigation — a fresh visit, a refresh, a link from outside the app — it has no idea what a slot &quot;was previously showing.&quot; It can only know that from client-side navigation history. So it needs a fallback per slot to fall back to when it has nothing else to go on.\u003C\u002Fp>",{"id":192,"code":193,"type":137,"language":138,"highlight":194},"b28","\u002F\u002F app\u002Fdashboard\u002F@analytics\u002Fdefault.tsx\nexport default function Default() {\n  return null; \u002F\u002F or a placeholder — whatever the slot should look like when idle\n}",[],{"id":196,"html":197,"type":83},"b29","\u003Cp>Skip this file, and a hard navigation to a route that doesn&#39;t explicitly fill every slot renders a 404 for the whole page — not a silently empty slot, a 404. This is the single most common first bug with parallel routes, and it looks nothing like its cause: a page that works fine when you click into it from elsewhere in the app, then 404s the instant you hit refresh.\u003C\u002Fp>",{"id":199,"html":200,"text":200,"type":91,"level":31},"b30","Stage 3: intercepting the photo route into the slot",{"id":202,"html":203,"type":83},"b31","\u003Cp>Now the actual feature. The full, real photo page lives at its own route:\u003C\u002Fp>",{"id":205,"code":206,"type":137,"language":176,"highlight":207},"b32","app\u002F\n  photo\u002F\n    [id]\u002F\n      page.tsx        # the real, standalone page for a direct visit\n  feed\u002F\n    layout.tsx\n    page.tsx           # the grid — links to \u002Fphoto\u002F[id]\n    @modal\u002F\n      default.tsx       # renders null — no modal open\n      (.)photo\u002F\n        [id]\u002F\n          page.tsx      # the INTERCEPTED version, rendered into @modal",[],{"id":209,"html":210,"type":83},"b33","\u003Cp>The folder \u003Ccode>(.)photo\u003C\u002Fcode> sits inside \u003Ccode>feed\u002F@modal\u003C\u002Fcode>. Reading the dot convention: \u003Ccode>(.)\u003C\u002Fcode> matches a segment at the \u003Cem>same level\u003C\u002Fem> — and because \u003Ccode>@modal\u003C\u002Fcode> is a slot, not a segment, &quot;the same level&quot; here means the same level as \u003Ccode>feed\u003C\u002Fcode> itself. That&#39;s the detail from the mental model section made concrete: if \u003Ccode>@modal\u003C\u002Fcode> counted as a level, you&#39;d reach for \u003Ccode>(..)\u003C\u002Fcode> instead, and it would be wrong.\u003C\u002Fp>",{"id":212,"code":213,"type":137,"language":138,"highlight":214},"b34","\u002F\u002F app\u002Ffeed\u002F@modal\u002F(.)photo\u002F[id]\u002Fpage.tsx\nimport { PhotoModal } from \"@\u002Fcomponents\u002Fphoto-modal\";\n\nexport default async function InterceptedPhoto({\n  params,\n}: {\n  params: Promise\u003C{ id: string }>;\n}) {\n  const { id } = await params;\n  return \u003CPhotoModal id={id} \u002F>;\n}",[],{"id":216,"code":217,"type":137,"language":138,"highlight":218},"b35","\u002F\u002F app\u002Ffeed\u002Flayout.tsx\nexport default function FeedLayout({\n  children,\n  modal,\n}: {\n  children: React.ReactNode;\n  modal: React.ReactNode;\n}) {\n  return (\n    \u003C>\n      {children}\n      {modal}\n    \u003C\u002F>\n  );\n}",[],{"id":220,"html":221,"type":83},"b36","\u003Cp>Click a \u003Ccode>&lt;Link href=&quot;\u002Fphoto\u002F42&quot;&gt;\u003C\u002Fcode> from inside the feed, and Next.js&#39;s client-side router resolves it \u003Cem>through\u003C\u002Fem> the interception: the URL becomes \u003Ccode>\u002Fphoto\u002F42\u003C\u002Fcode>, but the component that renders is the one in \u003Ccode>@modal\u002F(.)photo\u002F[id]\u002Fpage.tsx\u003C\u002Fcode> — layered over the still-mounted feed. Paste that same \u003Ccode>\u002Fphoto\u002F42\u003C\u002Fcode> URL into a new tab, or hit refresh while it&#39;s open, and there&#39;s no &quot;previous client-side location&quot; to intercept from — Next.js renders the real \u003Ccode>app\u002Fphoto\u002F[id]\u002Fpage.tsx\u003C\u002Fcode> instead, full-page, no feed underneath.\u003C\u002Fp>",{"id":223,"html":224,"type":83},"b37","\u003Cp>\u003Cstrong>Key concept:\u003C\u002Fstrong> the interception only fires for a client-side navigation whose \u003Cem>previous\u003C\u002Fem> route matches the dot-convention target. The URL is identical either way; only how you arrived decides which component runs. That&#39;s what makes the link genuinely shareable — the person you send it to always gets the real, full page, never a modal with no feed behind it.\u003C\u002Fp>",{"id":226,"html":227,"text":227,"type":91,"level":31},"b38","Stage 4: closing the modal",{"id":229,"html":230,"type":83},"b39","\u003Cp>Closing is just a navigation back to a URL that doesn&#39;t render the intercepted route — most simply, the browser back button, or a \u003Ccode>&lt;Link&gt;\u003C\u002Fcode> back to \u003Ccode>\u002Ffeed\u003C\u002Fcode>, or \u003Ccode>router.back()\u003C\u002Fcode> from a close button:\u003C\u002Fp>",{"id":232,"code":233,"type":137,"language":138,"highlight":234},"b40","\u002F\u002F app\u002Ffeed\u002F@modal\u002F(.)photo\u002F[id]\u002Fpage.tsx (excerpt)\n\"use client\";\nimport { useRouter } from \"next\u002Fnavigation\";\n\nfunction CloseButton() {\n  const router = useRouter();\n  return \u003Cbutton onClick={() => router.back()}>Close\u003C\u002Fbutton>;\n}",[],{"id":236,"html":237,"type":83},"b41","\u003Cp>Once the route no longer matches \u003Ccode>(.)photo\u002F[id]\u003C\u002Fcode>, the \u003Ccode>@modal\u003C\u002Fcode> slot falls back to its \u003Ccode>default.tsx\u003C\u002Fcode> — which renders \u003Ccode>null\u003C\u002Fcode> — and the modal disappears while the feed underneath was never unmounted.\u003C\u002Fp>",{"id":239,"html":240,"type":83},"b42","\u003C!-- playground:start -->",{"id":242,"html":243,"text":243,"type":91,"level":31},"b43","🎮 Try it yourself",{"id":245,"html":246,"type":83},"b44","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-parallel-intercepting-routes-modals\u002Fplayground\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":248,"html":249,"type":83},"b45","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":251,"html":252,"type":83},"b46","\u003C!-- playground:end -->",{"id":254,"html":255,"text":255,"type":91,"level":31},"b47","Edge cases and gotchas",{"id":257,"type":97,"items":258,"ordered":18},"b48",[259,260,261,262],"\u003Cstrong>Slots don&#39;t count toward the dot level.\u003C\u002Fstrong> This is the error that produces no error message — the interception simply never fires, and a link just does an ordinary full navigation. If a dot convention &quot;should&quot; work by folder depth but silently doesn&#39;t, recount the levels using only real route segments, ignoring every \u003Ccode>@slot\u003C\u002Fcode> folder in between.","\u003Cstrong>A missing \u003Ccode>default.tsx\u003C\u002Fcode> 404s on hard navigation, not on client navigation.\u003C\u002Fstrong> Test parallel routes with an actual page refresh, not just by clicking around — clicking around is exactly the case that already works.","\u003Cstrong>Parallel slots render sequentially within their shared layout, not concurrently with each other in the sense of wall-clock overlap on the server\u003C\u002Fstrong> — each one still needs its own render pass. Three heavy slots are three render passes, not one; give the expensive ones their own \u003Ccode>loading.tsx\u003C\u002Fcode> so the cheap ones don&#39;t wait behind them.","\u003Cstrong>Route groups aren&#39;t slots.\u003C\u002Fstrong> A folder in parentheses without an \u003Ccode>@\u003C\u002Fcode>, like \u003Ccode>(marketing)\u003C\u002Fcode>, organizes routes without adding a segment — a different feature that happens to share the &quot;doesn&#39;t affect the URL&quot; property. Don&#39;t reach for one when you mean the other.",{"id":264,"html":265,"text":265,"type":91,"level":31},"b49","Best practices",{"id":267,"type":97,"items":268,"ordered":18},"b50",[269,270,271,272],"\u003Cstrong>Reach for this when the UI is genuinely two things at once\u003C\u002Fstrong>: a list and an overlay detail, a page and a login prompt, a cart and a drawer — cases where the underlying page must stay mounted and the overlay needs its own shareable URL.","\u003Cstrong>Skip it for UI that has no reason to be a URL\u003C\u002Fstrong> — a confirm-delete dialog, a tooltip, a dropdown. Reaching for parallel + intercepting routes there is solving a problem you don&#39;t have; plain component state is simpler and correct.","\u003Cstrong>Always ship the real route.\u003C\u002Fstrong> The whole value of this pattern comes from the full page at \u003Ccode>\u002Fphoto\u002F[id]\u003C\u002Fcode> existing and being correct on its own — never make it a stub that assumes it&#39;s always reached through the modal.","\u003Cstrong>Give each slot its own loading and error boundaries\u003C\u002Fstrong> rather than one boundary for the whole layout — that&#39;s what lets, say, an analytics panel stream independently of a sidebar that&#39;s already ready.",{"id":274,"html":275,"text":275,"type":91,"level":31},"b51","FAQ",{"id":277,"html":278,"text":278,"type":91,"level":43},"b52","Do parallel route slots show up in the URL?",{"id":280,"html":281,"type":83},"b53","\u003Cp>No. A slot folder (\u003Ccode>@modal\u003C\u002Fcode>, \u003Ccode>@analytics\u003C\u002Fcode>) is a prop-passing mechanism for the layout above it, not a route a visitor can navigate to, and it adds no segment to the URL.\u003C\u002Fp>",{"id":283,"html":284,"text":285,"type":91,"level":43},"b54","What happens if I forget \u003Ccode>default.tsx\u003C\u002Fcode> in a slot?","What happens if I forget default.tsx in a slot?",{"id":287,"html":288,"type":83},"b55","\u003Cp>A hard navigation (a fresh visit or a refresh) to any route that doesn&#39;t explicitly fill that slot renders a 404 for the whole page, because Next.js has no client-side history to fall back on and no explicit fallback to use instead.\u003C\u002Fp>",{"id":290,"html":291,"text":291,"type":91,"level":43},"b56","Can I nest intercepting routes more than one dot deep?",{"id":293,"html":294,"type":83},"b57","\u003Cp>Yes — \u003Ccode>(..)\u003C\u002Fcode> for one segment up, \u003Ccode>(..)(..)\u003C\u002Fcode> for two, and \u003Ccode>(...)\u003C\u002Fcode> to intercept all the way from the app&#39;s root, however many real segments that spans. Count only actual route segments; folders that are slots or route groups don&#39;t add to the count.\u003C\u002Fp>",{"id":296,"html":297,"text":297,"type":91,"level":43},"b58","Is this the same as a client-side modal library?",{"id":299,"html":300,"type":83},"b59","\u003Cp>No, and that&#39;s the point. A client-side modal (a portal plus some open\u002Fclose state) has no URL of its own — refreshing or sharing the page loses the modal entirely. Parallel + intercepting routes give the modal a real route, so it survives a refresh and can be shared as a link, while still rendering as an overlay when reached by clicking through the app.\u003C\u002Fp>",{"id":302,"html":303,"text":303,"type":91,"level":43},"b60","Does this work with the Pages Router?",{"id":305,"html":306,"type":83},"b61","\u003Cp>No — parallel routes and intercepting routes are App Router conventions. A Pages Router app building this same pattern has to reach for a client-side modal library or a custom routing layer instead; there&#39;s no filesystem equivalent to migrate.\u003C\u002Fp>",{"id":308,"html":309,"text":309,"type":91,"level":31},"b62","Cheat sheet",{"id":311,"head":312,"rows":316,"type":345},"b63",[313,314,315],"Convention","Syntax","What it does",[317,321,325,329,333,337,341],[318,319,320],"Parallel route slot","\u003Ccode>@slotname\u002F\u003C\u002Fcode> folder","Adds a named prop to the parent layout; doesn&#39;t add a URL segment",[322,323,324],"Slot fallback","\u003Ccode>@slotname\u002Fdefault.tsx\u003C\u002Fcode>","Rendered when Next.js has no other match for the slot (required to avoid a 404 on hard navigation)",[326,327,328],"Intercept same level","\u003Ccode>(.)segment\u002F\u003C\u002Fcode>","Intercepts a route at the same level as the intercepting folder",[330,331,332],"Intercept one level up","\u003Ccode>(..)segment\u002F\u003C\u002Fcode>","Intercepts a route one segment above",[334,335,336],"Intercept two levels up","\u003Ccode>(..)(..)segment\u002F\u003C\u002Fcode>","Intercepts a route two segments above",[338,339,340],"Intercept from root","\u003Ccode>(...)segment\u002F\u003C\u002Fcode>","Intercepts a route from the app&#39;s root, regardless of depth",[342,343,344],"Route group (not a slot)","\u003Ccode>(name)\u002F\u003C\u002Fcode> folder","Organizes routes without a \u003Ccode>@\u003C\u002Fcode>; also adds no URL segment, but carries no slot prop","table",{"id":347,"code":348,"type":137,"language":138,"highlight":349},"b64","\u002F\u002F The whole pattern, minimal\n\u002F\u002F app\u002Fphoto\u002F[id]\u002Fpage.tsx            — the real, standalone route\n\u002F\u002F app\u002Ffeed\u002Flayout.tsx                — accepts `modal` as a prop\n\u002F\u002F app\u002Ffeed\u002F@modal\u002Fdefault.tsx        — returns null when idle\n\u002F\u002F app\u002Ffeed\u002F@modal\u002F(.)photo\u002F[id]\u002Fpage.tsx — the intercepted, modal version\n\nexport default function FeedLayout({ children, modal }: {\n  children: React.ReactNode;\n  modal: React.ReactNode;\n}) {\n  return (\u003C>{children}{modal}\u003C\u002F>);\n}",[],{"id":351,"html":352,"type":83},"b65","\u003C!-- quiz:start -->",{"id":354,"html":355,"text":355,"type":91,"level":31},"b66","🧠 Test yourself",{"id":357,"html":358,"type":83},"b67","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-parallel-intercepting-routes-modals\u002Fquiz\">Take the 8-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":360,"html":361,"type":83},"b68","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":363,"html":364,"type":83},"b69","\u003C!-- quiz:end -->",{"id":366,"html":367,"text":367,"type":91,"level":31},"b70","Key takeaways",{"id":369,"type":97,"items":370,"ordered":18},"b71",[371,372,373,374],"A parallel route slot (\u003Ccode>@slotname\u003C\u002Fcode>) is a named subtree passed to a layout as a prop — it never appears in the URL and doesn&#39;t count as a segment for anything else.","Every slot needs a \u003Ccode>default.tsx\u003C\u002Fcode>, or a hard navigation that doesn&#39;t fill it renders a 404 for the whole page.","Intercepting routes (\u003Ccode>(.)\u003C\u002Fcode>, \u003Ccode>(..)\u003C\u002Fcode>, \u003Ccode>(..)(..)\u003C\u002Fcode>, \u003Ccode>(...)\u003C\u002Fcode>) render different UI for the same URL depending on whether the user arrived by client-side navigation from a matching location or by a hard navigation — the URL itself never lies about what&#39;s really there.","Together, they build a modal, drawer, or overlay that is a real, shareable, refreshable route — not client state pretending to be one.",{"id":376,"html":377,"type":83},"b72","\u003Cp>That photo modal from the top of this article can now survive a refresh, get shared as a link, and still feel like an overlay to anyone who clicked their way there — because it was never one thing pretending to be another. It&#39;s a route, and a modal, at the same time, and the App Router&#39;s file conventions are what make that not a contradiction.\u003C\u002Fp>",{"id":379,"html":380,"type":83},"b73","\u003Cp>If you&#39;ve hit a case where the dot-level math didn&#39;t add up the way you expected, or a slot 404&#39;d on you before you found \u003Ccode>default.tsx\u003C\u002Fcode>, drop it in the comments — that&#39;s exactly the kind of gotcha worth comparing notes on.\u003C\u002Fp>",{"id":382,"html":383,"type":83},"b74","\u003Cp>Earlier in this series: \u003Ca href=\"https:\u002F\u002Fdev.to\u002Fparsajiravand\u002Fnextjs-cache-components-explained-with-cheat-sheet-55ob\">Next.js Cache Components Explained\u003C\u002Fa> covers how the App Router decides what&#39;s static, cached, or streamed — a good companion if you&#39;re deciding how each slot here should fetch its data. And \u003Ca href=\"https:\u002F\u002Fdev.to\u002Fparsajiravand\u002Fnextjs-server-actions-mutations-security-cheat-sheet-b63\">Next.js Server Actions: Mutations &amp; Security\u003C\u002Fa> is the natural next step if your modal needs to submit a mutation without a full page navigation.\u003C\u002Fp>",{"id":385,"type":386},"b75","divider",{"id":388,"html":389,"type":83},"b76","\u003Cp>🚀 \u003Cstrong>Want more like this?\u003C\u002Fstrong> Every guide, playground, and quiz lives on \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">bestpractic.org\u003C\u002Fa>\u003C\u002Fstrong> — open it and \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">sign up free\u003C\u002Fa>\u003C\u002Fstrong> so the next one finds you.\u003C\u002Fp>",{"id":391,"html":392,"type":83},"b77","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":394,"type":97,"items":395,"ordered":18},"b78",[396,397,398],"⭐ \u003Cstrong>GitHub\u003C\u002Fstrong> — follow me and star the projects: \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fparsajiravand\">github.com\u002Fparsajiravand\u003C\u002Fa>","💬 \u003Cstrong>Discord\u003C\u002Fstrong> — join the frontend best-practices community: \u003Ca href=\"https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ\">discord.gg\u002Fd9KRhuAwQ\u003C\u002Fa>","📸 \u003Cstrong>Instagram\u003C\u002Fstrong> — frontend best practices, daily: \u003Ca href=\"https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F\">@bestpractice___\u003C\u002Fa>","You build a photo grid. Clicking a thumbnail should pop up a modal with the full photo — the feed stays visible and scrolled to where the user left it. `useState` and a `{open && \u003CPhotoModal \u002F>}` conditional get this working in about ten minutes. Then someone refreshes the page while the modal is open, and the photo is just gone — back to the bare feed, because that boolean lived in memory and the URL never knew a modal was open. Someone else shares the link expecting to send a specific photo, and it opens to... the feed. The modal was never a place; it was a client state flag.\n\nThis is one of the few UI problems the App Router's own routing model was built to solve, and it does it with two conventions that are easy to skim past in the docs and hard to use correctly from memory: **parallel routes** and **intercepting routes**. This article is written against **Next.js 16.3** (the current Active LTS release, verified against the framework's own file-convention docs and its GitHub releases in September 2026); the conventions below have been stable since Next.js 13 and are not part of the newer Cache Components model, so nothing here changes if you're on an app that hasn't adopted `cacheComponents` yet.\n\n## What you'll learn\n\nBy the end of this article you'll be able to:\n\n- Explain what a parallel route slot (`@slot`) actually is, and why it doesn't add a segment to the URL\n- Use `default.tsx` correctly, and explain exactly when Next.js needs it and why its absence produces a 404\n- Read the `(.)`, `(..)`, `(..)(..)`, and `(...)` intercepting-route matchers and know which one a given folder move needs\n- Combine both conventions to build a modal that has a real, shareable, refreshable URL\n- Recognize the difference between a client-side navigation into an intercepted route and a hard navigation to the same URL, and why they render different things on purpose\n\n## Who this is for\n\nYou've built at least a small App Router project — you know what `page.tsx` and `layout.tsx` do, and you've used `\u003CLink>` for client-side navigation. You don't need any prior experience with parallel or intercepting routes; we build both from nothing.\n\n## Table of contents\n\n- [The problem: a modal that isn't really a place](#the-problem-a-modal-that-isnt-really-a-place)\n- [The mental model: slots, and routes that fill them differently](#the-mental-model-slots-and-routes-that-fill-them-differently)\n- [Stage 1: a parallel route slot on its own](#stage-1-a-parallel-route-slot-on-its-own)\n- [Stage 2: default.tsx and the 404 it prevents](#stage-2-defaulttsx-and-the-404-it-prevents)\n- [Stage 3: intercepting the photo route into the slot](#stage-3-intercepting-the-photo-route-into-the-slot)\n- [Stage 4: closing the modal](#stage-4-closing-the-modal)\n- [Edge cases and gotchas](#edge-cases-and-gotchas)\n- [Best practices](#best-practices)\n- [FAQ](#faq)\n- [Cheat sheet](#cheat-sheet)\n- [Key takeaways](#key-takeaways)\n\n## The problem: a modal that isn't really a place\n\nHere's the naive version, and it's genuinely how most people reach for this first:\n\n```tsx\n\u002F\u002F app\u002Ffeed\u002Fpage.tsx — the \"wrong way first\"\n\"use client\";\nimport { useState } from \"react\";\n\nexport default function Feed() {\n  const [openPhoto, setOpenPhoto] = useState\u003Cstring | null>(null);\n\n  return (\n    \u003C>\n      \u003CPhotoGrid onSelect={(id) => setOpenPhoto(id)} \u002F>\n      {openPhoto && (\n        \u003CPhotoModal id={openPhoto} onClose={() => setOpenPhoto(null)} \u002F>\n      )}\n    \u003C\u002F>\n  );\n}\n```\n\nThis works exactly as long as the user never leaves the tab. The moment they do any of the following, it falls apart:\n\n- **Refresh the page.** `openPhoto` was never anywhere but React state — it's gone. The URL is still just `\u002Ffeed`.\n- **Share the link.** There's nothing to share; the modal was never addressable.\n- **Use the back button.** The browser doesn't know a modal was \"opened\" — there's no history entry for it.\n\nThe fix people reach for next is a separate route, `\u002Fphoto\u002F[id]\u002Fpage.tsx`. That solves the URL problem, but now clicking a thumbnail navigates *away* from the feed entirely — the grid, its scroll position, and any in-flight state are gone, replaced by a page whose whole job is to show one photo. You've traded \"not a real place\" for \"a real place that destroys the one you were just looking at.\"\n\nWhat you actually want is a route that is real — refreshable, shareable, back-button-able — but that, when reached by clicking a link *from the feed*, renders as an overlay on top of the feed instead of replacing it. That's not a state management problem. It's a routing problem, and Next.js has a routing answer.\n\n## The mental model: slots, and routes that fill them differently\n\n**The mental model:** a layout can have more than one independently-rendered subtree — Next.js calls each one a **slot**, written as a folder named `@slotname`. A slot is not a route segment; it doesn't appear in the URL and doesn't count as a level of nesting for anything else in the app. It exists purely so a layout can accept several pieces of UI as named \"slots\" and place them wherever it wants, each one navigable on its own.\n\nAn **intercepting route** is the second, separate piece: a way for one route to say \"when the user gets to me by clicking a link from somewhere specific, render *this* UI instead of the destination's normal page — but if they land on me any other way (a fresh visit, a refresh, a shared link), render the real thing.\" The folder name encodes *how far away* \"somewhere specific\" is, using a dot convention measured in route segments: `(.)` the same level, `(..)` one level up, `(..)(..)` two levels up, `(...)` all the way from the app's root. Because slots aren't segments, they don't count when you're counting dots — this is the detail that trips people up first, and it's covered below.\n\nPut together: the feed's layout gets a `@modal` slot. Normally that slot renders nothing. A link to `\u002Fphoto\u002F[id]` from inside the feed gets *intercepted* and rendered into the `@modal` slot as an overlay — same URL, same address bar, same shareable link, but rendered as a modal because of *how* the user arrived. Land on `\u002Fphoto\u002F[id]` directly, and the intercepting route steps aside; the real, full `\u002Fphoto\u002F[id]\u002Fpage.tsx` renders instead.\n\n## Stage 1: a parallel route slot on its own\n\nStart with just the slot mechanic, no interception yet. A layout can declare extra props beyond `children` by naming folders `@something`:\n\n```\napp\u002F\n  dashboard\u002F\n    layout.tsx\n    page.tsx\n    @analytics\u002F\n      page.tsx\n    @team\u002F\n      page.tsx\n```\n\n```tsx\n\u002F\u002F app\u002Fdashboard\u002Flayout.tsx\nexport default function DashboardLayout({\n  children,\n  analytics,\n  team,\n}: {\n  children: React.ReactNode;\n  analytics: React.ReactNode;\n  team: React.ReactNode;\n}) {\n  return (\n    \u003Cdiv className=\"dashboard-grid\">\n      \u003Cmain>{children}\u003C\u002Fmain>\n      \u003Caside>{analytics}\u003C\u002Faside>\n      \u003Caside>{team}\u003C\u002Faside>\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n**Key concept:** `@analytics` and `@team` are props on the layout, matched by folder name, not routes a visitor can navigate to directly. Each one is its own subtree with its own `loading.tsx` and `error.tsx` if you want them — the analytics panel can stream in behind its own `\u003CSuspense>` boundary while the team panel is already sitting there rendered, because Next.js renders each slot independently. This alone is useful even with zero interception: it's how you give one section of a page its own loading and error state without wrapping the whole route in a single boundary.\n\n## Stage 2: default.tsx and the 404 it prevents\n\nSlots need a `default.tsx` for a specific reason: Next.js has to render *something* in every slot on every request, and on a hard navigation — a fresh visit, a refresh, a link from outside the app — it has no idea what a slot \"was previously showing.\" It can only know that from client-side navigation history. So it needs a fallback per slot to fall back to when it has nothing else to go on.\n\n```tsx\n\u002F\u002F app\u002Fdashboard\u002F@analytics\u002Fdefault.tsx\nexport default function Default() {\n  return null; \u002F\u002F or a placeholder — whatever the slot should look like when idle\n}\n```\n\nSkip this file, and a hard navigation to a route that doesn't explicitly fill every slot renders a 404 for the whole page — not a silently empty slot, a 404. This is the single most common first bug with parallel routes, and it looks nothing like its cause: a page that works fine when you click into it from elsewhere in the app, then 404s the instant you hit refresh.\n\n## Stage 3: intercepting the photo route into the slot\n\nNow the actual feature. The full, real photo page lives at its own route:\n\n```\napp\u002F\n  photo\u002F\n    [id]\u002F\n      page.tsx        # the real, standalone page for a direct visit\n  feed\u002F\n    layout.tsx\n    page.tsx           # the grid — links to \u002Fphoto\u002F[id]\n    @modal\u002F\n      default.tsx       # renders null — no modal open\n      (.)photo\u002F\n        [id]\u002F\n          page.tsx      # the INTERCEPTED version, rendered into @modal\n```\n\nThe folder `(.)photo` sits inside `feed\u002F@modal`. Reading the dot convention: `(.)` matches a segment at the *same level* — and because `@modal` is a slot, not a segment, \"the same level\" here means the same level as `feed` itself. That's the detail from the mental model section made concrete: if `@modal` counted as a level, you'd reach for `(..)` instead, and it would be wrong.\n\n```tsx\n\u002F\u002F app\u002Ffeed\u002F@modal\u002F(.)photo\u002F[id]\u002Fpage.tsx\nimport { PhotoModal } from \"@\u002Fcomponents\u002Fphoto-modal\";\n\nexport default async function InterceptedPhoto({\n  params,\n}: {\n  params: Promise\u003C{ id: string }>;\n}) {\n  const { id } = await params;\n  return \u003CPhotoModal id={id} \u002F>;\n}\n```\n\n```tsx\n\u002F\u002F app\u002Ffeed\u002Flayout.tsx\nexport default function FeedLayout({\n  children,\n  modal,\n}: {\n  children: React.ReactNode;\n  modal: React.ReactNode;\n}) {\n  return (\n    \u003C>\n      {children}\n      {modal}\n    \u003C\u002F>\n  );\n}\n```\n\nClick a `\u003CLink href=\"\u002Fphoto\u002F42\">` from inside the feed, and Next.js's client-side router resolves it *through* the interception: the URL becomes `\u002Fphoto\u002F42`, but the component that renders is the one in `@modal\u002F(.)photo\u002F[id]\u002Fpage.tsx` — layered over the still-mounted feed. Paste that same `\u002Fphoto\u002F42` URL into a new tab, or hit refresh while it's open, and there's no \"previous client-side location\" to intercept from — Next.js renders the real `app\u002Fphoto\u002F[id]\u002Fpage.tsx` instead, full-page, no feed underneath.\n\n**Key concept:** the interception only fires for a client-side navigation whose *previous* route matches the dot-convention target. The URL is identical either way; only how you arrived decides which component runs. That's what makes the link genuinely shareable — the person you send it to always gets the real, full page, never a modal with no feed behind it.\n\n## Stage 4: closing the modal\n\nClosing is just a navigation back to a URL that doesn't render the intercepted route — most simply, the browser back button, or a `\u003CLink>` back to `\u002Ffeed`, or `router.back()` from a close button:\n\n```tsx\n\u002F\u002F app\u002Ffeed\u002F@modal\u002F(.)photo\u002F[id]\u002Fpage.tsx (excerpt)\n\"use client\";\nimport { useRouter } from \"next\u002Fnavigation\";\n\nfunction CloseButton() {\n  const router = useRouter();\n  return \u003Cbutton onClick={() => router.back()}>Close\u003C\u002Fbutton>;\n}\n```\n\nOnce the route no longer matches `(.)photo\u002F[id]`, the `@modal` slot falls back to its `default.tsx` — which renders `null` — and the modal disappears while the feed underneath was never unmounted.\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-parallel-intercepting-routes-modals\u002Fplayground)**\n\n_Runs right in your browser — poke at it and watch the concept react live._\n\n\u003C!-- playground:end -->\n\n## Edge cases and gotchas\n\n- **Slots don't count toward the dot level.** This is the error that produces no error message — the interception simply never fires, and a link just does an ordinary full navigation. If a dot convention \"should\" work by folder depth but silently doesn't, recount the levels using only real route segments, ignoring every `@slot` folder in between.\n- **A missing `default.tsx` 404s on hard navigation, not on client navigation.** Test parallel routes with an actual page refresh, not just by clicking around — clicking around is exactly the case that already works.\n- **Parallel slots render sequentially within their shared layout, not concurrently with each other in the sense of wall-clock overlap on the server** — each one still needs its own render pass. Three heavy slots are three render passes, not one; give the expensive ones their own `loading.tsx` so the cheap ones don't wait behind them.\n- **Route groups aren't slots.** A folder in parentheses without an `@`, like `(marketing)`, organizes routes without adding a segment — a different feature that happens to share the \"doesn't affect the URL\" property. Don't reach for one when you mean the other.\n\n## Best practices\n\n- **Reach for this when the UI is genuinely two things at once**: a list and an overlay detail, a page and a login prompt, a cart and a drawer — cases where the underlying page must stay mounted and the overlay needs its own shareable URL.\n- **Skip it for UI that has no reason to be a URL** — a confirm-delete dialog, a tooltip, a dropdown. Reaching for parallel + intercepting routes there is solving a problem you don't have; plain component state is simpler and correct.\n- **Always ship the real route.** The whole value of this pattern comes from the full page at `\u002Fphoto\u002F[id]` existing and being correct on its own — never make it a stub that assumes it's always reached through the modal.\n- **Give each slot its own loading and error boundaries** rather than one boundary for the whole layout — that's what lets, say, an analytics panel stream independently of a sidebar that's already ready.\n\n## FAQ\n\n### Do parallel route slots show up in the URL?\n\nNo. A slot folder (`@modal`, `@analytics`) is a prop-passing mechanism for the layout above it, not a route a visitor can navigate to, and it adds no segment to the URL.\n\n### What happens if I forget `default.tsx` in a slot?\n\nA hard navigation (a fresh visit or a refresh) to any route that doesn't explicitly fill that slot renders a 404 for the whole page, because Next.js has no client-side history to fall back on and no explicit fallback to use instead.\n\n### Can I nest intercepting routes more than one dot deep?\n\nYes — `(..)` for one segment up, `(..)(..)` for two, and `(...)` to intercept all the way from the app's root, however many real segments that spans. Count only actual route segments; folders that are slots or route groups don't add to the count.\n\n### Is this the same as a client-side modal library?\n\nNo, and that's the point. A client-side modal (a portal plus some open\u002Fclose state) has no URL of its own — refreshing or sharing the page loses the modal entirely. Parallel + intercepting routes give the modal a real route, so it survives a refresh and can be shared as a link, while still rendering as an overlay when reached by clicking through the app.\n\n### Does this work with the Pages Router?\n\nNo — parallel routes and intercepting routes are App Router conventions. A Pages Router app building this same pattern has to reach for a client-side modal library or a custom routing layer instead; there's no filesystem equivalent to migrate.\n\n## Cheat sheet\n\n| Convention | Syntax | What it does |\n| --- | --- | --- |\n| Parallel route slot | `@slotname\u002F` folder | Adds a named prop to the parent layout; doesn't add a URL segment |\n| Slot fallback | `@slotname\u002Fdefault.tsx` | Rendered when Next.js has no other match for the slot (required to avoid a 404 on hard navigation) |\n| Intercept same level | `(.)segment\u002F` | Intercepts a route at the same level as the intercepting folder |\n| Intercept one level up | `(..)segment\u002F` | Intercepts a route one segment above |\n| Intercept two levels up | `(..)(..)segment\u002F` | Intercepts a route two segments above |\n| Intercept from root | `(...)segment\u002F` | Intercepts a route from the app's root, regardless of depth |\n| Route group (not a slot) | `(name)\u002F` folder | Organizes routes without a `@`; also adds no URL segment, but carries no slot prop |\n\n```tsx\n\u002F\u002F The whole pattern, minimal\n\u002F\u002F app\u002Fphoto\u002F[id]\u002Fpage.tsx            — the real, standalone route\n\u002F\u002F app\u002Ffeed\u002Flayout.tsx                — accepts `modal` as a prop\n\u002F\u002F app\u002Ffeed\u002F@modal\u002Fdefault.tsx        — returns null when idle\n\u002F\u002F app\u002Ffeed\u002F@modal\u002F(.)photo\u002F[id]\u002Fpage.tsx — the intercepted, modal version\n\nexport default function FeedLayout({ children, modal }: {\n  children: React.ReactNode;\n  modal: React.ReactNode;\n}) {\n  return (\u003C>{children}{modal}\u003C\u002F>);\n}\n```\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 8-question quiz →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-parallel-intercepting-routes-modals\u002Fquiz)**\n\n_Instant feedback, a hint on every question, and an explanation for each answer — right or wrong._\n\n\u003C!-- quiz:end -->\n\n## Key takeaways\n\n- A parallel route slot (`@slotname`) is a named subtree passed to a layout as a prop — it never appears in the URL and doesn't count as a segment for anything else.\n- Every slot needs a `default.tsx`, or a hard navigation that doesn't fill it renders a 404 for the whole page.\n- Intercepting routes (`(.)`, `(..)`, `(..)(..)`, `(...)`) render different UI for the same URL depending on whether the user arrived by client-side navigation from a matching location or by a hard navigation — the URL itself never lies about what's really there.\n- Together, they build a modal, drawer, or overlay that is a real, shareable, refreshable route — not client state pretending to be one.\n\nThat photo modal from the top of this article can now survive a refresh, get shared as a link, and still feel like an overlay to anyone who clicked their way there — because it was never one thing pretending to be another. It's a route, and a modal, at the same time, and the App Router's file conventions are what make that not a contradiction.\n\nIf you've hit a case where the dot-level math didn't add up the way you expected, or a slot 404'd on you before you found `default.tsx`, drop it in the comments — that's exactly the kind of gotcha worth comparing notes on.\n\nEarlier in this series: [Next.js Cache Components Explained](https:\u002F\u002Fdev.to\u002Fparsajiravand\u002Fnextjs-cache-components-explained-with-cheat-sheet-55ob) covers how the App Router decides what's static, cached, or streamed — a good companion if you're deciding how each slot here should fetch its data. And [Next.js Server Actions: Mutations & Security](https:\u002F\u002Fdev.to\u002Fparsajiravand\u002Fnextjs-server-actions-mutations-security-cheat-sheet-b63) is the natural next step if your modal needs to submit a mutation without a full page navigation.\n\n---\n\n🚀 **Want more like this?** Every guide, playground, and quiz lives on **[bestpractic.org](https:\u002F\u002Fbestpractic.org\u002F)** — open it and **[sign up free](https:\u002F\u002Fbestpractic.org\u002F)** so the next one finds you.\n\n*Thanks for reading! Let's stay connected:*\n\n- ⭐ **GitHub** — follow me and star the projects: [github.com\u002Fparsajiravand](https:\u002F\u002Fgithub.com\u002Fparsajiravand)\n- 💬 **Discord** — join the frontend best-practices community: [discord.gg\u002Fd9KRhuAwQ](https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ)\n- 📸 **Instagram** — frontend best practices, daily: [@bestpractice___](https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F)",{"title":47,"canonical":401,"description":49},"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-parallel-intercepting-routes-modals","01a09181-2f3b-7349-b652-026ce8b965e3",{"name":404,"part":43,"total":43,"items":405},"Next.js Deep Dive",[406,410,415],{"slug":407,"title":408,"publishedAt":409,"readingMinutes":51},"nextjs-weekly-cache-components-explained","Next.js Cache Components Explained (with Cheat Sheet)","2026-09-01T11:24:15.087Z",{"slug":411,"title":412,"publishedAt":413,"readingMinutes":414},"nextjs-weekly-server-actions-mutations-security","Next.js Server Actions: Mutations & Security (Cheat Sheet)","2026-09-08T10:57:00.975Z",15,{"slug":46,"title":47,"publishedAt":52,"readingMinutes":51},{"id":417,"locked":18},"01a09181-2f78-760e-825b-f912ef202dca",[419],{"id":420,"slug":46,"title":421,"_count":422},"01a09181-2f98-7125-8f53-c5833a1abec9","Next.js Parallel & Intercepting Routes",{"questions":423},8,[425],{"locale":13,"slug":46},{"id":420,"slug":46,"title":421,"_count":427,"questionCount":423},{"questions":423},[429,431,435,439,443,444,447,449,452,455,458,462],{"slug":69,"name":70,"articles":430},97,{"slug":432,"name":433,"articles":434},"javascript","Javascript",81,{"slug":436,"name":437,"articles":438},"frontend","Frontend",70,{"slug":440,"name":441,"articles":442},"css","Css",32,{"slug":72,"name":73,"articles":53},{"slug":445,"name":446,"articles":51},"typescript","Typescript",{"slug":66,"name":67,"articles":448},12,{"slug":450,"name":451,"articles":448},"performance","Performance",{"slug":453,"name":454,"articles":423},"browser","Browser",{"slug":456,"name":457,"articles":423},"node","Node",{"slug":459,"name":460,"articles":461},"grammar","Grammar",6,{"slug":463,"name":464,"articles":461},"programming","Programming",{"items":466,"meta":509},[467,479,494],{"id":45,"slug":46,"title":47,"subtitle":48,"excerpt":49,"coverUrl":50,"locale":13,"readingMinutes":51,"publishedAt":52,"viewCount":468,"likeCount":19,"commentCount":19,"author":469,"vertical":470,"topic":471,"tags":472,"_count":477,"playground":478,"hasQuiz":17,"hasPlayground":17},28,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[473,474,475,476],{"slug":61,"name":62,"color":48},{"slug":66,"name":67,"color":48},{"slug":69,"name":70,"color":48},{"slug":72,"name":73,"color":48},{"assessments":75},{"slug":46},{"id":480,"slug":411,"title":412,"subtitle":48,"excerpt":481,"coverUrl":482,"locale":13,"readingMinutes":414,"publishedAt":413,"viewCount":483,"likeCount":19,"commentCount":19,"author":484,"vertical":485,"topic":486,"tags":487,"_count":492,"playground":493,"hasQuiz":17,"hasPlayground":17},"01a06d6c-41f6-77bd-95e3-cdd69a0f72eb","Next.js Server Actions look like plain functions but compile to public POST endpoints. Learn the mutation flow, built-in CSRF checks, and the auth you owe.","\u002Fmedia\u002Fcovers\u002Fnextjs-weekly-server-actions-mutations-security.png",187,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[488,489,490,491],{"slug":61,"name":62,"color":48},{"slug":66,"name":67,"color":48},{"slug":69,"name":70,"color":48},{"slug":72,"name":73,"color":48},{"assessments":75},{"slug":411},{"id":495,"slug":407,"title":408,"subtitle":48,"excerpt":496,"coverUrl":497,"locale":13,"readingMinutes":51,"publishedAt":409,"viewCount":498,"likeCount":19,"commentCount":19,"author":499,"vertical":500,"topic":501,"tags":502,"_count":507,"playground":508,"hasQuiz":17,"hasPlayground":17},"01a0499d-b520-778f-8286-8221ec03b8b5","How Next.js Cache Components decide what's static, what's cached, and what streams — the use cache directive, cacheLife, and Suspense explained.","\u002Fmedia\u002Fcovers\u002Fnextjs-weekly-cache-components-explained.png",258,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[503,504,505,506],{"slug":61,"name":62,"color":48},{"slug":66,"name":67,"color":48},{"slug":69,"name":70,"color":48},{"slug":72,"name":73,"color":48},{"assessments":75},{"slug":407},{"page":75,"perPage":510,"total":43,"totalPages":75},7,"\u003Cdiv class=\"shj shj-lang-tsx shj-multiline\" data-lang=\"tsx\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003Cdiv>10\u003C\u002Fdiv>\u003Cdiv>11\u003C\u002Fdiv>\u003Cdiv>12\u003C\u002Fdiv>\u003Cdiv>13\u003C\u002Fdiv>\u003Cdiv>14\u003C\u002Fdiv>\u003Cdiv>15\u003C\u002Fdiv>\u003Cdiv>16\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-cmnt\">\u002F\u002F app\u002Ffeed\u002Fpage.tsx — the \"wrong way first\"\n\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"use client\"\u003C\u002Fspan>;\n\u003Cspan class=\"shj-kwd\">import\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> useState \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">from\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"react\"\u003C\u002Fspan>;\n\n\u003Cspan class=\"shj-kwd\">export\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">default\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-class\">Feed\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">[\u003C\u002Fspan>openPhoto\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> setOpenPhoto\u003Cspan class=\"shj-bracket\">]\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> useState\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>string \u003Cspan class=\"shj-oper\">|\u003C\u002Fspan> \u003Cspan class=\"shj-num\">null\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-num\">null\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\n  \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\n    \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\n      \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">PhotoGrid\u003C\u002Fspan> \u003Cspan class=\"shj-class\">onSelect\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">={\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>id\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-func\">setOpenPhoto\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>id\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">}\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">\u002F&gt;\u003C\u002Fspan>\n      \u003Cspan class=\"shj-oper\">{\u003C\u002Fspan>openPhoto \u003Cspan class=\"shj-oper\">&#38;&#38;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\n        \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">PhotoModal\u003C\u002Fspan> \u003Cspan class=\"shj-class\">id\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">={\u003C\u002Fspan>openPhoto\u003Cspan class=\"shj-oper\">}\u003C\u002Fspan> \u003Cspan class=\"shj-class\">onClose\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">={\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-func\">setOpenPhoto\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-num\">null\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">}\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">\u002F&gt;\u003C\u002Fspan>\n      \u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">}\u003C\u002Fspan>\n    \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\n  \u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-plain shj-multiline\" data-lang=\"plain\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">app\u002F\n  dashboard\u002F\n    layout.tsx\n    page.tsx\n    @analytics\u002F\n      page.tsx\n    @team\u002F\n      page.tsx\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-tsx shj-multiline\" data-lang=\"tsx\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003Cdiv>10\u003C\u002Fdiv>\u003Cdiv>11\u003C\u002Fdiv>\u003Cdiv>12\u003C\u002Fdiv>\u003Cdiv>13\u003C\u002Fdiv>\u003Cdiv>14\u003C\u002Fdiv>\u003Cdiv>15\u003C\u002Fdiv>\u003Cdiv>16\u003C\u002Fdiv>\u003Cdiv>17\u003C\u002Fdiv>\u003Cdiv>18\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-cmnt\">\u002F\u002F app\u002Fdashboard\u002Flayout.tsx\n\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">export\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">default\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-class\">DashboardLayout\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  children\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n  analytics\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n  team\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  children\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-class\">React\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-class\">ReactNode\u003C\u002Fspan>;\n  analytics\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-class\">React\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-class\">ReactNode\u003C\u002Fspan>;\n  team\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-class\">React\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-class\">ReactNode\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\n    \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">div\u003C\u002Fspan> \u003Cspan class=\"shj-class\">className\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"dashboard-grid\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\n      \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">main\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">{\u003C\u002Fspan>children\u003Cspan class=\"shj-oper\">}\u003C\u002Fspan>\u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-var\">main\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\n      \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">aside\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">{\u003C\u002Fspan>analytics\u003Cspan class=\"shj-oper\">}\u003C\u002Fspan>\u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-var\">aside\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\n      \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">aside\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">{\u003C\u002Fspan>team\u003Cspan class=\"shj-oper\">}\u003C\u002Fspan>\u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-var\">aside\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\n    \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-var\">div\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\n  \u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-tsx shj-multiline\" data-lang=\"tsx\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-cmnt\">\u002F\u002F app\u002Fdashboard\u002F@analytics\u002Fdefault.tsx\n\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">export\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">default\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-class\">Default\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan> \u003Cspan class=\"shj-num\">null\u003C\u002Fspan>; \u003Cspan class=\"shj-cmnt\">\u002F\u002F or a placeholder — whatever the slot should look like when idle\n\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-plain shj-multiline\" data-lang=\"plain\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003Cdiv>10\u003C\u002Fdiv>\u003Cdiv>11\u003C\u002Fdiv>\u003Cdiv>12\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">app\u002F\n  photo\u002F\n    [id]\u002F\n      page.tsx        # the real, standalone page for a direct visit\n  feed\u002F\n    layout.tsx\n    page.tsx           # the grid — links to \u002Fphoto\u002F[id]\n    @modal\u002F\n      default.tsx       # renders null — no modal open\n      (.)photo\u002F\n        [id]\u002F\n          page.tsx      # the INTERCEPTED version, rendered into @modal\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-tsx shj-multiline\" data-lang=\"tsx\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003Cdiv>10\u003C\u002Fdiv>\u003Cdiv>11\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-cmnt\">\u002F\u002F app\u002Ffeed\u002F@modal\u002F(.)photo\u002F[id]\u002Fpage.tsx\n\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">import\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> \u003Cspan class=\"shj-class\">PhotoModal\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">from\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"@\u002Fcomponents\u002Fphoto-modal\"\u003C\u002Fspan>;\n\n\u003Cspan class=\"shj-kwd\">export\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">default\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">async\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-class\">InterceptedPhoto\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  params\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  params\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-class\">Promise\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> id\u003Cspan class=\"shj-type\">: string\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> id \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">await\u003C\u002Fspan> params;\n  \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan> \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">PhotoModal\u003C\u002Fspan> \u003Cspan class=\"shj-class\">id\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">={\u003C\u002Fspan>id\u003Cspan class=\"shj-oper\">}\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">\u002F&gt;\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-tsx shj-multiline\" data-lang=\"tsx\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003Cdiv>10\u003C\u002Fdiv>\u003Cdiv>11\u003C\u002Fdiv>\u003Cdiv>12\u003C\u002Fdiv>\u003Cdiv>13\u003C\u002Fdiv>\u003Cdiv>14\u003C\u002Fdiv>\u003Cdiv>15\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-cmnt\">\u002F\u002F app\u002Ffeed\u002Flayout.tsx\n\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">export\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">default\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-class\">FeedLayout\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  children\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n  modal\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  children\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-class\">React\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-class\">ReactNode\u003C\u002Fspan>;\n  modal\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-class\">React\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-class\">ReactNode\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\n    \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\n      \u003Cspan class=\"shj-oper\">{\u003C\u002Fspan>children\u003Cspan class=\"shj-oper\">}\u003C\u002Fspan>\n      \u003Cspan class=\"shj-oper\">{\u003C\u002Fspan>modal\u003Cspan class=\"shj-oper\">}\u003C\u002Fspan>\n    \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\n  \u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-tsx shj-multiline\" data-lang=\"tsx\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-cmnt\">\u002F\u002F app\u002Ffeed\u002F@modal\u002F(.)photo\u002F[id]\u002Fpage.tsx (excerpt)\n\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"use client\"\u003C\u002Fspan>;\n\u003Cspan class=\"shj-kwd\">import\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> useRouter \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">from\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"next\u002Fnavigation\"\u003C\u002Fspan>;\n\n\u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-class\">CloseButton\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> router \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-func\">useRouter\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan> \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">button\u003C\u002Fspan> \u003Cspan class=\"shj-class\">onClick\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">={\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> router\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">back\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">}\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>Close\u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-var\">button\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-tsx shj-multiline\" data-lang=\"tsx\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003Cdiv>10\u003C\u002Fdiv>\u003Cdiv>11\u003C\u002Fdiv>\u003Cdiv>12\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-cmnt\">\u002F\u002F The whole pattern, minimal\n\u003C\u002Fspan>\u003Cspan class=\"shj-cmnt\">\u002F\u002F app\u002Fphoto\u002F[id]\u002Fpage.tsx            — the real, standalone route\n\u003C\u002Fspan>\u003Cspan class=\"shj-cmnt\">\u002F\u002F app\u002Ffeed\u002Flayout.tsx                — accepts `modal` as a prop\n\u003C\u002Fspan>\u003Cspan class=\"shj-cmnt\">\u002F\u002F app\u002Ffeed\u002F@modal\u002Fdefault.tsx        — returns null when idle\n\u003C\u002Fspan>\u003Cspan class=\"shj-cmnt\">\u002F\u002F app\u002Ffeed\u002F@modal\u002F(.)photo\u002F[id]\u002Fpage.tsx — the intercepted, modal version\n\u003C\u002Fspan>\n\u003Cspan class=\"shj-kwd\">export\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">default\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-class\">FeedLayout\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> children\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> modal \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  children\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-class\">React\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-class\">ReactNode\u003C\u002Fspan>;\n  modal\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-class\">React\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-class\">ReactNode\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">{\u003C\u002Fspan>children\u003Cspan class=\"shj-oper\">}\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">{\u003C\u002Fspan>modal\u003Cspan class=\"shj-oper\">}\u003C\u002Fspan>\u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>",{"locked":18,"total":19,"comments":521},[]]