Parallel and Intercepting Routes in Next.js
How parallel slots and intercepting routes power dashboards, modals, and tabbed UIs in the Next.js App Router — the file conventions, when to reach for each, and the patterns that hold up in production.
What you'll learn
- ✓How @slot folders enable multiple page views in one layout
- ✓How (.) (..) (...) intercepting conventions work
- ✓The classic "modal with shareable URL" pattern
- ✓Where these features bite back
Prerequisites
- •Solid understanding of layouts and route segments
What and Why
Most apps eventually need two things that traditional file-based routers struggle with. First, multiple views in one layout — a dashboard with a feed, a sidebar, and a notifications panel that each have their own loading and error states. Second, modals that share a URL — clicking a photo opens a modal overlay, but visiting that same URL directly shows the full page.
Parallel routes and intercepting routes are the App Router’s answers. Parallel routes let one layout render several independently-streamed pages. Intercepting routes let one URL render as a modal in one context and as a full page in another.
Mental Model
A parallel route is a folder prefixed with @. Inside a layout, each @slot becomes a prop. So app/dashboard/layout.tsx receives children, @analytics, and @team if those folders exist. Each slot has its own page.tsx, loading.tsx, and error.tsx. They render concurrently, fail independently, and stream separately.
An intercepting route uses parentheses-with-dots: (.) for same level, (..) one level up, (..)(..) two levels up, (...) from the root. The folder @modal/(..)photo/[id] says: “when the user navigates to /photo/123 from this layout, render this page instead of the real one.” Direct visits still hit the real route.
Hands-on Example
Build a photo grid where clicking a photo opens a modal, but /photo/123 directly loads the full page.
app/
layout.tsx // renders {children} and {modal}
@modal/
default.tsx // returns null when no modal active
(..)photo/
[id]/
page.tsx // the modal UI
page.tsx // the photo grid
photo/
[id]/
page.tsx // the full photo page
The root layout consumes both slots:
// app/layout.tsx
export default function RootLayout({ children, modal }) {
return (
<html><body>
{children}
{modal}
</body></html>
);
}
In the grid, link with <Link href="/photo/123">. Because the user is coming from the root layout, the (..)photo/[id] intercepting route matches first and renders the modal. Reloading the page or sharing the URL bypasses the interception and renders the full app/photo/[id]/page.tsx.
Click <Link href="/photo/123"> from grid
|
v
Router checks for intercepting match in active layout
|-- found: @modal/(..)photo/[id]/page.tsx
|
v
[Render] RootLayout
children = grid (unchanged)
modal = ModalPhoto(123) <- overlay
Direct visit / reload of /photo/123
|
v
No interception (no active layout slot to host it)
|
v
[Render] RootLayout
children = app/photo/[id]/page.tsx <- full page
modal = @modal/default.tsx (null) For a pure parallel-routes case (no interception), think of a dashboard:
app/dashboard/
layout.tsx // uses {children}, {analytics}, {team}
page.tsx // main feed
@analytics/page.tsx
@team/page.tsx
Each slot streams independently. A slow @analytics does not block @team.
Common Pitfalls
The first gotcha is missing default.tsx. Every parallel slot needs a default.tsx to handle navigations where that slot has no explicit match — without it, a soft navigation can blank the slot or 404 the whole layout. The fix is a one-line export default function Default() { return null; }.
Second, counting segments wrong with (..). The dots count route segments, not file-system folders. Groups like (marketing) do not count. People burn an hour on this; double-check by reading the URL path the route resolves to.
Third, interception only on soft navigation. A full reload, an external link, or an opened-in-new-tab URL all bypass interception. Your modal page must therefore be a reasonable standalone page too. Design for both at once.
Fourth, state sync between slots. Slots render independently, so a write in one (e.g. starring a photo in @modal) does not automatically refresh the other. Use revalidatePath or router.refresh() after mutations.
Best Practices
Reach for parallel routes when slots have genuinely independent data and lifecycles — dashboards, split panels, conditional UI based on auth state. Always ship a default.tsx per slot from day one. Use intercepting routes for the modal-with-URL pattern and resist using them as a general routing trick; the cognitive overhead is real. Keep the intercepted page and the real page rendering the same data so refresh-vs-click feels consistent. Finally, write a short comment in each @slot README pointing to where the slot is consumed — six months later you will thank yourself.
Wrap-up
Parallel routes give you concurrent, independently-streamed views in one layout. Intercepting routes give you context-aware rendering for the same URL. Used carefully, they unlock UI patterns that used to require client-side state machines and brittle modals. Used carelessly, the file conventions become a maze — so start small, name slots clearly, and always include the defaults.
Related articles
- Next.js Next.js API Routes: Best Practices and Patterns
Build robust Next.js API routes with proper validation, error handling, authentication, rate limiting, and testing patterns for production apps.
- Next.js Next.js i18n: Complete Internationalization Guide
Build multilingual Next.js apps with the App Router using locale routing, server-side translations, dynamic language switching, and SEO best practices.
- Next.js Next.js Server Actions: Forms and Mutations Guide
Learn how to use Next.js Server Actions for form handling, data mutations, optimistic updates, and error handling with practical examples.
- Next.js Next.js Streaming and Loading UI: Instant Pages
Build instant-loading Next.js pages with streaming, Suspense boundaries, loading.tsx files, and skeleton UIs for the best user experience.