Parallel Routes and Intercepting Routes in the Next.js App Router: Field Notes on the Photo-Modal Pattern
Table of Contents
About the Author

Ahmed Mahmoud
Author & Developer
Software engineer passionate about web development and user experience design.
Founder of Devya · eng-ahmed.com ↗Key takeaways
At Devya, we use parallel routes — folders in the Next.js App Router prefixed with @, like @modal — to render more than one page in the same layout at once. Next.js passes each slot's content into the nearest layout as a named prop, alongside the implicit children slot, and slot folders never add a segment to the URL.
We pair parallel routes with intercepting routes — the (.), (..), (..)(..), and (...) folder conventions — to swap a different component into a slot on a client-side navigation, without changing the URL at all.
A default.tsx file is what Next.js renders for a slot when the current URL doesn't match anything inside it. Without one, a hard navigation to a route that doesn't fill every slot returns a 404 — a bug we shipped once and only caught on refresh.
A hard refresh or a shared link to a route like /photo/123 is not supposed to show the modal. It's supposed to render the plain page.tsx for that route as a full, independent page. That's the entire point of the pattern for us, not something to work around.
Every slot is its own subtree with its own loading.tsx and error.tsx, so we can let a modal suspend and stream independently of the page behind it.
What problem do parallel routes solve for our team?
A parallel route lets our layout render more than one page at the same time, each addressed by a named slot instead of a URL segment. We reached for this pattern on a client project with a photo grid: a thumbnail click needed to open a lightbox without leaving the grid, and the lightbox also needed a shareable URL of its own. A boolean-driven modal gives you the first half for free and cannot give you the second half at all, because there is no URL for state that lives in useState.
The folder convention is a name prefixed with @. Next.js passes each slot's matched content into the nearest layout.tsx as a prop named after the folder. We've reused the same mechanism outside of modals too — a dashboard with @team and @analytics panes that navigate independently, where switching tabs in one pane leaves the other exactly as it was.
How does the (.) convention decide what our team's links intercept?
The dot-segment conventions match a target route relative to where the intercepting file sits in the file system, not relative to the current URL. (.) matches a route at the same folder level, (..) matches one level above, (..)(..) matches two levels above, and (...) matches from the app root.
When a route like app/@modal/(.)photo/[id]/page.tsx exists, a client-side navigation to /photo/[id] triggered from a route at the same level as @modal renders into the modal slot instead of replacing the whole tree. A fresh page load of the same URL — a refresh, a pasted link, an external referral — resolves the ordinary app/photo/[id]/page.tsx instead. Interception only ever fires on a client-side transition.
Why does default.tsx matter, and what breaks without it?
We skipped it once and everything looked fine in development until someone on the team refreshed a route with the modal open and hit a 404. A slot with no matching segment for the current URL needs something to render on a full page load, since there is no prior client state to fall back to. default.tsx, returning null, is that fallback.
Client-side navigation is more forgiving: without a default.tsx, Next.js keeps rendering whatever a slot last showed instead of unmounting it. That's useful for the dashboard-tabs case, but it also means a missing default.tsx passes every manual click-through test and only fails on a hard reload — precisely the path a shared link takes.
Why does a refresh show the full page instead of the modal?
Because that's the design, not a defect. app/photo/[id]/page.tsx is a complete, independent page — same content, no modal chrome, rendered without any client JavaScript running first. That's what keeps the URL genuinely shareable and indexable: a crawler or a link-preview bot that never executes your router gets the real page, not an empty shell waiting on JavaScript.
How do we close the modal without losing the ability to share its URL?
Our modal is a Client Component that calls router.back() from next/navigation, which reverses the soft navigation and lets the slot fall back to default.tsx. The gap we found in production: a visitor opening /photo/123 directly in a new tab has no history entry to go back to, so router.back() does nothing or exits the app. We now always render an explicit close link with a real href back to the gallery, so closing never depends on history existing.
Client-state modal or intercepting-route modal — how do we decide?
A boolean-state modal costs one useState call, has no URL, doesn't survive a refresh, and needs client JavaScript to render anything. An intercepting-route modal costs a slot, an intercepting folder, a fallback route, and a default.tsx — and gets a real URL, a working refresh, and content that renders without JavaScript on direct navigation. We still use the boolean for genuinely disposable UI like a confirmation prompt. We reach for the routed version when the content behind the modal deserves its own address.
FAQ
Do parallel route slot folders like @modal show up in the URL? No. The @ prefix marks a folder as a slot, not a route segment, so it never appears in the URL and only determines which layout prop receives its content.
What happens if someone navigates straight to /photo/123 instead of clicking through the feed? Next.js renders app/photo/[id]/page.tsx as an ordinary full page. Interception only fires on a client-side transition from a matching route.
Can a parallel route slot have its own loading.tsx? Yes. Each slot is an independent subtree and can define its own loading.tsx, error.tsx, and not-found.tsx.
Why doesn't router.back() close the modal for a visitor who opened the link directly? There's no history entry to go back to when a route is the first thing loaded in a tab. We pair the close handler with an explicit fallback link.
Can (..)(..) be combined with a slot defined several levels up the tree? Yes — the dot count only describes how many folder levels above the intercepting file's own location the target route sits, independent of where the @slot folder itself lives.
Further Reading
Frontend Engineering
Temporal in Production: Replacing JavaScript Date, Fixing the Same-Day Bug, and the RSC Boundary Nobody Warns You About
Temporal is the TC39 API that replaces JavaScript's Date object with immutable, time-zone-aware types. We migrated a Next.js application's date handling onto Temporal, and these are our field notes: which Temporal type maps to which field, why Temporal objects cannot be passed as Server Component props, and how to keep the Postgres round-trip honest.
Frontend Engineering
next/image in Next.js 16: What Is Not Automatic About Image Optimization
Using next/image is not the same as having fast images. The component lazy-loads every image it renders unless you pass priority, and a wrong sizes prop makes a 400-pixel card download the 3840-pixel candidate. We collected the parts our team has to configure by hand.