Next.js Parallel Routes and Intercepting Routes for Modals
Master Next.js parallel routes and intercepting routes to build smooth modal patterns. Learn how to handle slot navigation for partial UI updates today.
When I first tried to implement a photo gallery modal in the App Router, I naively used a client-side state variable to toggle visibility. It worked fine until I realized that clicking "back" in the browser closed the entire page instead of just the modal. That's when I dove deep into Next.js parallel routes and intercepting routes, which finally gave me the SPA-like feel I was chasing.
Using these features together allows you to render multiple pages in the same layout simultaneously, effectively creating "slots" for your content.
Understanding Next.js Parallel Routes and Intercepting Routes
At its core, next.js parallel routes allow you to render one or more pages in the same layout. You define these using the @folder convention. When you combine these with next.js intercepting routes (the (.) folder convention), you can intercept a route that would normally trigger a full navigation and render it as a slot instead.
The magic happens when the URL changes. If a user clicks a link to /photo/123, Next.js intercepts the request, renders the photo view inside a parallel slot, and updates the URL without unmounting the main page. If the user refreshes the page, the intercepting route is ignored, and the standard /photo/123 page loads.
The Modal Pattern Architecture
When building a modal, you're essentially orchestrating three things:
- The base page layout.
- The parallel slot (e.g.,
@modal). - The intercepted route that fills that slot.
Here is how the directory structure typically looks:
TEXTapp/ layout.js page.js @modal/ (.)photo/[id]/ page.js default.js photo/[id]/ page.js
The default.js file in the @modal slot is critical. It tells Next.js what to render when the route isn't active—usually null. Without it, you'll run into 404s when navigating away from the modal route.
Implementing Slot Navigation
To make this work, your layout.js needs to accept the slots as props. This is a common pattern in React component composition: Mastering the Slot Pattern for Cleaner Code, but applied at the route level.
JSX// app/layout.js export default function RootLayout({ children, modal }) { return ( <html> <body> {children} {modal} </body> </html> ); }
When you navigate to the photo page, Next.js injects the contents of @modal/(.)photo/[id]/page.js into the {modal} slot. Because this is a standard navigation, you're handling browser history: Building SPA Navigation in React automatically. The URL updates, and the back button works exactly as expected.
Comparison of Navigation Strategies
| Strategy | URL Change | Full Page Load | Modal State |
|---|---|---|---|
| Standard Link | Yes | Yes | N/A |
| Client-side State | No | No | Lost on Refresh |
| Parallel/Intercepting | Yes | No | Persistent |
Avoiding Common Pitfalls
The most common issue I see is developers forgetting that the intercepted route must match the structure of the actual destination page. If your main page is at /photo/[id], your intercepting folder must be (.)photo/[id].
Also, don't forget to handle the "dismiss" action. Since the modal is just a route, you can close it by navigating back:
JSXCE9178">'use client'; import { useRouter } from CE9178">'next/navigation'; export default function Modal({ children }) { const router = useRouter(); return ( <div className="overlay" onClick={() => router.back()}> {children} </div> ); }
This approach feels much cleaner than managing global "isModalOpen" state. It respects the browser's history stack and ensures that if a user shares the URL, the recipient gets the full-page version of the content rather than an empty modal container.
One thing I'm still keeping an eye on is the performance impact of deep-nesting parallel slots. While it's great for UX, having too many active slots can lead to complex data-fetching scenarios. If you're doing heavy lifting, make sure you're keeping an eye on your Next.js App Router Parallel Data Fetching with Suspense to keep the UI snappy.
I'd suggest starting small. Try implementing a simple login modal or a detail view. Once you grasp how the slots behave in the layout, the complexity of more advanced next.js modal patterns becomes much easier to manage.
FAQ
Why does my modal disappear when I refresh? That's expected behavior for intercepting routes. If you want the modal to persist on refresh, you'd need to implement a conditional render based on search params, but intercepting routes are specifically designed to serve the full page on a hard refresh.
Can I have multiple modals open at once?
Yes, by defining multiple slots (e.g., @modal and @sidebar), you can intercept different routes into different slots, effectively managing multiple partial UI updates simultaneously.
Does this work with Server Actions? Absolutely. Since these are just standard routes in the App Router, you can use Server Actions inside your intercepted pages to handle form submissions or data mutations just like you would on a standard page.
I’m still experimenting with how these patterns interact with complex loading.js states. Sometimes the transition feels a bit jarring if the parallel slot loads significantly slower than the parent page. For now, I'm leaning heavily into skeleton states within the slots to mask the latency.