Intercepting routes solve a classic problem: clicking a thumbnail should open a modal, but visiting the URL directly should show a full page.
| Prefix | Meaning |
|---|---|
(.) | Match the same level |
(..) | Match one level up |
(..)(..) | Match two levels up |
(...) | Match from the app root |
These are folder names, not file names.
We want /photo/:id to:
/feedapp/
feed/
page.tsx
@modal/
(.)photo/[id]/page.tsx ← intercepts /photo/:id when nav happens from /feed
default.tsx
layout.tsx ← renders {children} and {modal}
photo/[id]/page.tsx ← the regular, full-page version
1// app/feed/layout.tsx
2export default function FeedLayout({ children, modal }: {
3 children: React.ReactNode;
4 modal: React.ReactNode;
5}) {
6 return (
7 <>
8 {children}
9 {modal} {/* renders the intercepted modal when present */}
10 </>
11 );
12}1// app/feed/@modal/(.)photo/[id]/page.tsx
2import { Dialog } from "@/components/ui/dialog";
3import { getPhoto } from "@/lib/photos";
4
5export default async function PhotoModal({ params }: { params: Promise<{ id: string }> }) {
6 const { id } = await params;
7 const photo = await getPhoto(id);
8 return (
9 <Dialog open>
10 <img src={photo.src} alt={photo.caption} />
11 <p>{photo.caption}</p>
12 </Dialog>
13 );
14}If you navigate to /feed directly, the modal slot has nothing to render:
1// app/feed/@modal/default.tsx
2export default function Default() {
3 return null;
4}1import Link from "next/link";
2
3<Link href={`/photo/${photo.id}`}> {/* navigated → modal opens */}
4 <img src={photo.thumb} alt="" />
5</Link>A right-click → "open in new tab" loads the full page route at app/photo/[id]/page.tsx. The URL is the same.
Push back to the parent route:
1"use client";
2import { useRouter } from "next/navigation";
3
4export function CloseButton() {
5 const router = useRouter();
6 return <button onClick={() => router.back()}>Close</button>;
7}