Combine Parallel Routes and Intercepting Routes by creating a parallel slot (e.g., @auth) with an intercepting route inside it that renders a modal, while maintaining a separate dedicated page for direct access.
Folder Structure
app/
├── layout.tsx # Root layout that renders both @auth slot and children
├── @auth/ # Parallel route slot
│ ├── (.)login/ # Intercepting route (intercepts /login)
│ │ └── page.tsx # Modal version of login
│ ├── default.tsx # Returns null when slot is inactive
│ └── page.tsx # Returns null on root route
└── login/
└── page.tsx # Dedicated /login page (direct access)
Implementation Steps
1. Create the dedicated page (app/login/page.tsx):
import { Login } from '@/app/ui/login'
export default function Page() {
return <Login />
}
2. Create the parallel slot default (app/@auth/default.tsx):
export default function Default() {
return null
}
3. Create the intercepting route (app/@auth/(.)login/page.tsx):
import { Modal } from '@/app/ui/modal'
import { Login } from '@/app/ui/login'
export default function Page() {
return (
<Modal>
<Login />
</Modal>
)
}
4. Configure parent layout (app/layout.tsx):
export default function Layout({
auth,
children,
}: {
auth: React.ReactNode
children: React.ReactNode
}) {
return (
<>
<nav>
<Link href="/login">Open modal</Link>
</nav>
<div>{auth}</div>
<div>{children}</div>
</>
)
}
5. Implement modal with close (app/ui/modal.tsx):
'use client'
import { useRouter } from 'next/navigation'
export function Modal({ children }: { children: React.ReactNode }) {
const router = useRouter()
return (
<>
<button onClick={() => router.back()}>Close modal</button>
<div>{children}</div>
</>
)
}
6. Handle slot closure (app/@auth/page.tsx):
export default function Page() {
return null
}
How It Works
- Client-side navigation to
/login: Intercepts and renders modal in @auth slot
- Direct access to
/login (page refresh, direct URL): Renders dedicated page
- Back navigation: Closes modal using
router.back()
- Navigate away: Slot renders null, modal disappears
Key insight: The (.) matcher means the route is one segment level higher, not filesystem level. Since slots (@auth) don't count as segments, (.)login intercepts /login even though it's two filesystem levels deep.
Sources: