# Parallel & Intercepting Routes Parallel routes render multiple pages in the same layout. Intercepting routes show a different UI when navigating from within your app vs direct URL access. Together they enable modal patterns. ## File Structure ``` app/ ├── @modal/ # Parallel route slot │ ├── default.tsx # Required! Returns null │ ├── (.)photos/ # Intercepts /photos/* │ │ └── [id]/ │ │ └── page.tsx # Modal content │ └── [...]catchall/ # Optional: catch unmatched │ └── page.tsx ├── photos/ │ └── [id]/ │ └── page.tsx # Full page (direct access) ├── layout.tsx # Renders both children and @modal └── page.tsx ``` ## Step 1: Root Layout with Slot ```tsx // app/layout.tsx export default function RootLayout({ children, modal, }: { children: React.ReactNode; modal: React.ReactNode; }) { return ( {children} {modal} ); } ``` ## Step 2: Default File (Critical!) **Every parallel route slot MUST have a `default.tsx`** to prevent 404s on hard navigation. ```tsx // app/@modal/default.tsx export default function Default() { return null; } ``` Without this file, refreshing any page will 404 because Next.js can't determine what to render in the `@modal` slot. ## Step 3: Intercepting Route (Modal) The `(.)` prefix intercepts routes at the same level. ```tsx // 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 ( {photo.title} ); } ``` ## Step 4: Full Page (Direct Access) ```tsx // 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 (
{photo.title}

{photo.title}

); } ``` ## Step 5: Modal Component with Correct Closing **Critical: Use `router.back()` to close modals, NOT `router.push()` or ``.** ```tsx // components/modal.tsx 'use client'; import { useRouter } from 'next/navigation'; import { useCallback, useEffect, useRef } from 'react'; export function Modal({ children }: { children: React.ReactNode }) { const router = useRouter(); const overlayRef = useRef(null); // Close on escape key useEffect(() => { function onKeyDown(e: KeyboardEvent) { if (e.key === 'Escape') { router.back(); // Correct } } document.addEventListener('keydown', onKeyDown); return () => document.removeEventListener('keydown', onKeyDown); }, [router]); // Close on overlay click const handleOverlayClick = useCallback((e: React.MouseEvent) => { if (e.target === overlayRef.current) { router.back(); // Correct } }, [router]); return (
{children}
); } ``` ### Why NOT `router.push('/')` or ``? Using `push` or `Link` to "close" a modal: 1. Adds a new history entry (back button shows modal again) 2. Doesn't properly clear the intercepted route 3. Can cause the modal to flash or persist unexpectedly `router.back()` correctly: 1. Removes the intercepted route from history 2. Returns to the previous page 3. Properly unmounts the modal ## Route Matcher Reference Matchers match **route segments**, not filesystem paths: | Matcher | Matches | Example | |---------|---------|---------| | `(.)` | Same level | `@modal/(.)photos` intercepts `/photos` | | `(..)` | One level up | `@modal/(..)settings` from `/dashboard/@modal` intercepts `/settings` | | `(..)(..)` | Two levels up | Rarely used | | `(...)` | From root | `@modal/(...)photos` intercepts `/photos` from anywhere | **Common mistake**: Thinking `(..)` means "parent folder" - it means "parent route segment". ## Handling Hard Navigation When users directly visit `/photos/123` (bookmark, refresh, shared link): - The intercepting route is bypassed - The full `photos/[id]/page.tsx` renders - Modal doesn't appear (expected behavior) If you want the modal to appear on direct access too, you need additional logic: ```tsx // app/photos/[id]/page.tsx import { Modal } from '@/components/modal'; export default async function PhotoPage({ params }) { const { id } = await params; const photo = await getPhoto(id); // Option: Render as modal on direct access too return ( {photo.title} ); } ``` ## Common Gotchas ### 1. Missing `default.tsx` → 404 on Refresh Every `@slot` folder needs a `default.tsx` that returns `null` (or appropriate content). ### 2. Modal Persists After Navigation You're using `router.push()` instead of `router.back()`. ### 3. Nested Parallel Routes Need Defaults Too If you have `@modal` inside a route group, each level needs its own `default.tsx`: ``` app/ ├── (marketing)/ │ ├── @modal/ │ │ └── default.tsx # Needed! │ └── layout.tsx └── layout.tsx ``` ### 4. Intercepted Route Shows Wrong Content Check your matcher: - `(.)photos` intercepts `/photos` from the same route level - If your `@modal` is in `app/dashboard/@modal`, use `(.)photos` to intercept `/dashboard/photos`, not `/photos` ### 5. TypeScript Errors with `params` In Next.js 15+, `params` is a Promise: ```tsx // Correct export default async function Page({ params }: { params: Promise<{ id: string }> }) { const { id } = await params; } ``` ## Complete Example: Photo Gallery Modal ``` app/ ├── @modal/ │ ├── default.tsx │ └── (.)photos/ │ └── [id]/ │ └── page.tsx ├── photos/ │ ├── page.tsx # Gallery grid │ └── [id]/ │ └── page.tsx # Full photo page ├── layout.tsx └── page.tsx ``` Links in the gallery: ```tsx // app/photos/page.tsx import Link from 'next/link'; export default async function Gallery() { const photos = await getPhotos(); return (
{photos.map(photo => ( {photo.title} ))}
); } ``` Clicking a photo → Modal opens (intercepted) Direct URL → Full page renders Refresh while modal open → Full page renders