Suggest an editImprove this articleRefine the answer for “Parallel routes and intercepting routes in Next.js”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Parallel routes** let you render multiple independent pages in one Next.js layout using named `@folder` slots, each with its own loading and error states. **Intercepting routes** use `(.)folder` syntax to capture navigation to a child segment and render a modal instead. ```tsx // app/feed/@modal/(.)photo/[id]/page.tsx export default async function PhotoModal({ params }) { const { id } = await params; const photo = await getPhoto(id); return <Modal><img src={photo.url} alt={photo.title} /></Modal>; } ``` **Key point:** parallel routes for independent panels with separate data fetching, intercepting routes for URL-addressable modals that close with the back button.Shown above the full answer for quick recall.Answer (EN)Image**Parallel routes** render multiple independent pages inside one layout using named `@folder` slots. Intercepting routes capture navigation to a child segment and render a modal or overlay instead, while the URL either stays on the parent or changes to the intercepted path depending on how the user got there. ## Theory ### TL;DR - Parallel routes use `@folder` syntax and are passed into a layout as separate React props - Each slot fetches data independently and has its own `loading.tsx` and `error.tsx` - Intercepting routes use `(.)folder`, `(..)folder`, or `(...)folder` to hijack navigation to a matched segment - Parallel routes are for multi-panel dashboards; intercepting routes are for modals that close with the browser back button - Together they power the Instagram-style photo modal: modal on client navigation, full page on direct URL visit ### Quick Example ```tsx // app/dashboard/layout.tsx export default function DashboardLayout({ children, stats, notifications, }: { children: React.ReactNode; stats: React.ReactNode; // app/dashboard/@stats/page.tsx notifications: React.ReactNode; // app/dashboard/@notifications/page.tsx }) { return ( <div className="flex h-screen"> <aside className="w-80 border-r">{stats}</aside> <div className="flex-1 flex flex-col"> <header>{notifications}</header> <main>{children}</main> </div> </div> ); } ``` The `stats` and `notifications` slots fetch data in parallel. If `stats` throws, `notifications` and `children` still render fine, as long as each slot has its own `error.tsx`. ### Key Difference Parallel routes fill named slots from sibling folders at the same URL level. They render simultaneously, each as a full React Server Component subtree with independent data fetching. Intercepting routes do not add a new place in the layout. They take over rendering for a matched segment and show their component instead, which means the same URL can render two completely different UIs depending on whether you navigated there via a link or typed it directly. ### When to Use - Multi-panel dashboard where panels call different APIs: parallel routes - Modal or drawer that opens over a page and should close with the back button: intercepting routes - Auth-conditional layout (dashboard vs login at the same URL): parallel routes with conditional rendering - Search overlay that appears as a modal via link but as a full page on direct visit: combine both Skip parallel routes for simple two-column layouts you can handle with CSS. Skip intercepting routes for modals that do not need a shareable URL. ### How Slots Work Folders named with `@` are slots. Next.js passes them as props to the nearest parent layout. The folder name becomes the prop name: `@stats` becomes the `stats` prop. ``` app/dashboard/ layout.tsx <- receives { children, stats, notifications } page.tsx <- becomes children @stats/ page.tsx <- becomes the stats prop loading.tsx <- only shows while stats is loading error.tsx <- only catches errors inside stats @notifications/ page.tsx <- becomes the notifications prop error.tsx ``` One thing that trips people up in production: if you forget `default.tsx` in a slot, Next.js throws a 404 when navigating to a URL where that slot has no matching segment. A `default.tsx` that returns `null` is enough to fix it. ### Intercepting Routes: Convention and Segment Matching The prefix controls which level the interception targets: ``` (.)photo - intercepts a segment at the same level (..)photo - intercepts one level up (..)(..)photo - two levels up (...)photo - intercepts from the app root ``` The canonical example: a `/feed` page has a `@modal` slot. Inside that slot, `(.)photo/[id]/page.tsx` intercepts navigation to `/photo/[id]` when it originates from within `/feed`. The URL changes to `/photo/123`, but Next.js renders the modal component, not the full photo page. Open `/photo/123` directly and the interception does not apply. ### Comparison Table | Feature | Parallel Routes | Intercepting Routes | |---|---|---| | Syntax | `@slot/page.tsx` | `(.)slot/page.tsx`, `(..)slot/page.tsx` | | Renders | Multiple slots in layout simultaneously | Replaces child segment rendering | | URL behavior | Each slot corresponds to its folder path | URL changes but rendering is overridden | | Data fetching | Fully independent per slot | Shares parent context | | Error isolation | Per-slot `error.tsx` | Needs `error.tsx` in the intercept folder | | Use case | Dashboards, conditional auth screens | Photo modals, login overlays, search panels | ### How Next.js Handles This Internally At build time, Next.js scans `@folder` names and generates slot props for layouts as part of the App Router file-convention system. During navigation, RSC fetches for each slot run in parallel with no waterfall. Intercepting routes work via segment matching priority: `(.)` matches the current path segment, `(..)` moves up one level, and the intercepted component suspends the child route render until it is dismissed. ### Common Mistakes **Forgetting `default.tsx` in a slot** ```tsx // app/dashboard/@stats/ has no default.tsx // Navigate to /dashboard/settings and Next.js 404s // because @stats has no segment matching that URL // Fix: add app/dashboard/@stats/default.tsx export default function StatsDefault() { return null; } ``` **Missing per-slot `loading.tsx`** Without `loading.tsx` in each slot, a slow slot blocks the entire layout. Add `@stats/loading.tsx` and `@notifications/loading.tsx` separately. Each one becomes its own Suspense boundary. **Intercepting without the `(.)` prefix** ``` app/settings/profile/page.tsx <- child route, changes URL, no interception app/settings/(.)profile/page.tsx <- intercepts /settings/profile, shows modal ``` Without `(.)`, Next.js treats it as a regular nested route. The URL changes and the modal never appears. **No error handling in intercepting routes** ```tsx // app/settings/(.)profile/page.tsx // Throws if userId is invalid. Without error.tsx here, // the whole layout crashes instead of just this overlay. const user = await fetchUser(searchParams.userId); ``` Add `app/settings/(.)profile/error.tsx` to catch errors locally. The parent layout stays intact. **Linking directly to a slot URL** `<Link href="/dashboard/@stats">` treats `@stats` as a real URL segment, which breaks routing. Slots are layout props, not navigable paths. ### Real-World Usage - Vercel dashboard: parallel slots for analytics overview and usage details, each hitting separate APIs - Linear: intercepting routes for the quick search modal over any project view - GitHub: parallel slots for repo readme and issues in a single layout - Instagram-style feed: the canonical photo modal from Next.js docs, combining a `@modal` slot with `(.)photo/[id]` interception - Supabase dashboard: intercepting modals for the SQL editor overlay on top of the tables view ### Follow-up Questions **Q:** How does data fetching in a parallel slot differ from a nested page? **A:** Each slot runs its own `fetch()` as an independent RSC with no waterfall. A nested page layout blocks on the parent fetch before the child can render. **Q:** What URL stays when an intercepting route opens a modal? **A:** The URL changes to the intercepted path (e.g., `/photo/123`), but the layout renders the modal component. Navigating directly to that URL skips the interception and loads the full page. **Q:** What happens if a parallel slot throws and has no `error.tsx`? **A:** The error propagates up to the nearest error boundary. Without per-slot `error.tsx`, the whole layout errors out. With it, only that slot shows the error UI while everything else keeps rendering. **Q:** Can you combine parallel and intercepting routes? **A:** Yes, and this is the recommended pattern for photo modals. A `@modal` slot lives in the feed layout, and inside it `(.)photo/[id]` intercepts navigation to the photo page. The slot manages modal state; the interception handles segment matching. **Q:** Senior: How does `default.tsx` affect streaming in parallel slots, and when does it break it? **A:** `default.tsx` fills a slot when no segment matches the current URL. Next.js renders it eagerly without streaming. If `default.tsx` contains a heavy component or a slow import, it blocks the slot's stream even when the main content is already ready. Use `default.tsx` only for simple null returns or lightweight skeleton states. ## Examples ### Dashboard with Independent Panels A Vercel-style dashboard where the stats sidebar loads and errors independently from the main chart. ```tsx // app/dashboard/layout.tsx export default function DashboardLayout({ children, stats, notifications, }: { children: React.ReactNode; stats: React.ReactNode; notifications: React.ReactNode; }) { return ( <div className="flex h-screen"> <aside className="w-80 border-r p-4">{stats}</aside> <div className="flex-1 flex flex-col"> <header className="border-b p-4">{notifications}</header> <main className="flex-1 p-6">{children}</main> </div> </div> ); } // app/dashboard/@stats/page.tsx export default async function Stats() { const data = await fetch("https://api.example.com/stats", { cache: "no-store", }).then((r) => r.json()); return ( <div> <p>Requests: {data.requests}</p> <p>Errors: {data.errors}</p> </div> ); } // app/dashboard/@stats/error.tsx "use client"; export default function StatsError() { return <p>Could not load stats.</p>; // Only this slot errors } // app/dashboard/@stats/loading.tsx export default function StatsLoading() { return <div className="animate-pulse h-20 bg-gray-100 rounded" />; } ``` Each slot fetches independently. If the stats API is down, the main chart and notifications still render. ### Photo Modal with Intercepting Routes The Instagram pattern: client navigation shows a modal, direct URL visit shows the full page. ```tsx // File structure: // app/feed/page.tsx -> /feed (photo grid) // app/feed/@modal/(.)photo/[id]/page.tsx -> intercepts /photo/[id] from /feed // app/feed/@modal/default.tsx -> null (no modal by default) // app/photo/[id]/page.tsx -> /photo/123 (full page, direct visit) // app/feed/layout.tsx export default function FeedLayout({ children, modal, }: { children: React.ReactNode; modal: React.ReactNode; }) { return ( <> {children} {modal} </> ); } // app/feed/@modal/(.)photo/[id]/page.tsx export default async function PhotoModal({ params, }: { params: Promise<{ id: string }>; }) { const { id } = await params; const photo = await getPhoto(id); return ( <div className="fixed inset-0 bg-black/50 flex items-center justify-center"> <div className="bg-white rounded-lg p-4 max-w-2xl"> <img src={photo.url} alt={photo.title} className="w-full" /> <h2 className="mt-2 text-lg font-medium">{photo.title}</h2> </div> </div> ); } // app/photo/[id]/page.tsx export default async function PhotoPage({ params, }: { params: Promise<{ id: string }>; }) { const { id } = await params; const photo = await getPhoto(id); return ( <div className="max-w-4xl mx-auto"> <img src={photo.url} alt={photo.title} className="w-full" /> <h1 className="mt-4 text-2xl font-bold">{photo.title}</h1> <p className="mt-2 text-gray-600">{photo.description}</p> </div> ); } // app/feed/@modal/default.tsx export default function ModalDefault() { return null; } ``` Clicking a photo navigates to `/photo/123`. The `(.)photo` prefix intercepts that navigation inside the feed layout and renders the modal. Opening `/photo/123` directly skips the interception and loads the full page. ### Conditional Auth Layout ```tsx // app/layout.tsx import { getUser } from "@/lib/auth"; export default async function RootLayout({ children, dashboard, login, }: { children: React.ReactNode; dashboard: React.ReactNode; // app/@dashboard/page.tsx login: React.ReactNode; // app/@login/page.tsx }) { const user = await getUser(); // Each slot is a full component with its own data fetching. // The layout just picks which one renders. return ( <html lang="en"> <body>{user ? dashboard : login}</body> </html> ); } ``` Both `@dashboard` and `@login` are full page components with independent data and error boundaries. The layout decides which one renders based on the session check.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.