Intercepting Routes
Intercepting routes let you load a route within the current layout while preserving context. Perfect for modals that can also be accessed directly.
The Convention
Use parentheses with dots to intercept:
| Convention | Intercepts |
|---|---|
(.)folder | Same level |
(..)folder | One level up |
(..)(..)folder | Two levels up |
(...)folder | From root |
Basic Example: Photo Modal
app/
├── layout.tsx
├── page.tsx → Photo grid
├── @modal/
│ ├── default.tsx
│ └── (.)photos/[id]/
│ └── page.tsx → Intercepted: shows in modal
└── photos/
└── [id]/
└── page.tsx → Direct access: full page
When clicking a photo link from the grid:
(.)photos/[id]/page.tsxintercepts the navigation- Photo opens in a modal over the grid
- URL changes to
/photos/123 - Background (grid) is preserved
When accessing /photos/123 directly:
- No interception (hard navigation)
- Full page loads:
photos/[id]/page.tsx
Implementation
The Modal Slot
// app/@modal/default.tsx
export default function Default() {
return null
}
// app/@modal/(.)photos/[id]/page.tsx
import { Modal } from '@/components/modal'
export default async function PhotoModal({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
const photo = await getPhoto(id)
return (
<Modal>
<img src={photo.url} alt={photo.title} />
<h2>{photo.title}</h2>
</Modal>
)
}
The Full Page
// app/photos/[id]/page.tsx
export default async function PhotoPage({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
const photo = await getPhoto(id)
return (
<main>
<img src={photo.url} alt={photo.title} />
<h2>{photo.title}</h2>
<p>{photo.description}</p>
<Comments photoId={id} />
</main>
)
}
The Layout
// app/layout.tsx
export default function Layout({
children,
modal,
}: {
children: React.ReactNode
modal: React.ReactNode
}) {
return (
<html>
<body>
{children}
{modal}
</body>
</html>
)
}
The Modal Component
A basic modal with close functionality:
// components/modal.tsx
'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 flex items-center justify-center">
<div className="bg-white p-6 rounded-lg max-w-2xl">
<button
onClick={() => router.back()}
className="absolute top-4 right-4"
>
Close
</button>
{children}
</div>
</div>
)
}
Common Patterns
Login Modal
app/
├── layout.tsx
├── page.tsx
├── @auth/
│ ├── default.tsx
│ └── (.)login/
│ └── page.tsx → Modal login
└── login/
└── page.tsx → Full page login
Product Quick View
app/
├── products/
│ ├── page.tsx → Product grid
│ ├── @quickview/
│ │ ├── default.tsx
│ │ └── (.)products/[id]/
│ │ └── page.tsx → Quick view modal
│ └── [id]/
│ └── page.tsx → Full product page
When to Use Intercepting Routes
| Good Use Cases | Not Recommended |
|---|---|
| Image galleries | Multi-step forms |
| Quick previews | Complex interactions |
| Login/signup modals | Data entry flows |
| Share dialogs | Deeply nested routes |
Summary
- Intercepting routes capture navigation to show content in the current context
- Use
(.),(..), or(...)conventions based on route level - Combine with parallel routes (
@modal) for modals - Works only on soft navigation - refresh loads the actual route
- Provide both modal and full-page versions for best UX

