Next.js Parallel Routes and Intercepting Routes: A Complete Guide
Parallel routes and intercepting routes are among the most powerful App Router primitives. This guide explains what they do, when to use them, and how to avoid the common pitfalls.
Parallel Routes
Parallel routes let you render multiple pages simultaneously in the same layout. The canonical use case is a dashboard with independently loading slots — a sidebar, a main content area, and a right panel that each have their own loading and error states.
Basic Setup
Parallel route slots are defined using the @slotName folder convention:
app/
dashboard/
layout.tsx ← receives all slots as props
page.tsx ← default content for the layout
@analytics/
page.tsx ← the analytics slot
@notifications/
page.tsx ← the notifications slot
The layout receives slots as named props:
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
analytics,
notifications,
}: {
children: React.ReactNode
analytics: React.ReactNode
notifications: React.ReactNode
}) {
return (
<div className="grid grid-cols-[1fr_300px] gap-6">
<div>
{children}
{analytics}
</div>
<aside>{notifications}</aside>
</div>
)
}
Each slot renders independently — if @analytics is slow, @notifications renders immediately without waiting.
Default Slots
When navigating to a route that doesn't define a sub-page for a slot, Next.js looks for a default.tsx file:
app/dashboard/
@analytics/
page.tsx ← renders at /dashboard
revenue/
page.tsx ← renders at /dashboard/revenue
default.tsx ← fallback when navigating away
Without default.tsx, navigating between routes that don't all define the slot will result in a 404 for that slot.
When to Use Parallel Routes
- Dashboard with multiple independent data sections
- Side-by-side comparison views
- Split-panel editors
- Simultaneous loading of header, sidebar, and main content
Don't use parallel routes just to organise code. They add complexity — use them when the independent loading and error boundaries provide real user value.
Intercepting Routes
Intercepting routes let you load a page from a different route while keeping the current page visible in the background. The canonical example: clicking a photo in a feed shows the photo in a modal (intercepted route), but navigating directly to the photo URL renders it as a full page.
The Convention
Intercepting routes use (..) prefix notation that mirrors ../ in file paths:
(.) ← same segment
(..) ← one segment above
(..)(..) ← two segments above
(...) ← from the root
app/
feed/
page.tsx
@modal/
(.)photos/
[id]/
page.tsx ← intercepts /photos/[id] from within /feed
default.tsx ← null — no modal when not intercepting
photos/
[id]/
page.tsx ← full-page photo view
Implementation Pattern
The intercepted route (the modal view) and the full-page route share the same content — the difference is the surrounding UI:
// app/photos/[id]/page.tsx — full page
import { PhotoDetail } from '@/components/PhotoDetail'
import { getPhoto } from '@/lib/photos'
export default async function PhotoPage({ params }) {
const photo = await getPhoto(params.id)
return (
<div className="max-w-3xl mx-auto py-20">
<PhotoDetail photo={photo} />
</div>
)
}
// app/feed/@modal/(.)photos/[id]/page.tsx — modal intercept
import { PhotoDetail } from '@/components/PhotoDetail'
import { Modal } from '@/components/Modal'
import { getPhoto } from '@/lib/photos'
export default async function PhotoModal({ params }) {
const photo = await getPhoto(params.id)
return (
<Modal>
<PhotoDetail photo={photo} />
</Modal>
)
}
// components/Modal.tsx — closes on back navigation
'use client'
import { useRouter } from 'next/navigation'
export function Modal({ children }: { children: React.ReactNode }) {
const router = useRouter()
return (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center"
onClick={() => router.back()}>
<div className="bg-white rounded-2xl p-8 max-w-2xl w-full mx-4"
onClick={(e) => e.stopPropagation()}>
{children}
</div>
</div>
)
}
When to Use Intercepting Routes
- Photo/video galleries (Instagram-style modal on click, full page on direct URL)
- Product quick-view modals in e-commerce
- Login modal that keeps the current page in the background
- Detail panels without losing the list context
The Gotcha: Hard Navigation
Intercepting routes only work with client-side navigation (using <Link>). If a user pastes the URL directly, refreshes, or shares the link, they get the full-page route — which is the correct behaviour. Design your full-page routes to be complete, standalone pages.
Combining Both Patterns
A common pattern is combining parallel routes and intercepting routes:
app/
products/
page.tsx ← product grid
@modal/ ← parallel route slot for modals
default.tsx ← null
(.)products/
[id]/
page.tsx ← quick-view modal, intercepted from grid
[id]/
page.tsx ← full product detail page
The product grid renders at /products. Clicking a product shows the quick-view modal (intercepted). Navigating directly to /products/123 renders the full detail page. Sharing the URL always gives a functional full-page view.
These are among the most expressive routing primitives available in any web framework. Used appropriately, they eliminate entire categories of state management complexity around modals, drawers, and split-panel UIs.
Continue reading
Related articles
Rate Limiting in Next.js: Protecting Your API Routes
How to implement production-grade rate limiting in Next.js — with Middleware-level protection, per-user limits, and distributed rate limiting using Upstash Redis.
EngineeringVercel vs Netlify vs AWS Amplify for Next.js in 2026
A practical comparison of the three most common Next.js hosting platforms — Vercel, Netlify, and AWS Amplify — with real cost and capability trade-offs.
EngineeringHow to Structure a Next.js Project for Scale
A battle-tested folder structure for Next.js App Router projects — from small MVPs to large SaaS applications with multiple teams.
Stay informed
Get our monthly deep dives.
Engineering, design, and growth insights — once a month. No spam.
Browse all resources