Skip to content
C.W.K.
Stream
Lesson 08 of 10 · published

Intercepting Routes

~22 min · intercepting routes, modal, overlay

Level 0Curious
0 XP0/68 lessons0/11 achievements
0/120 XP to next level120 XP to go0% complete

The Instagram pattern

Click a photo in a feed: a modal opens with the photo. Refresh the page on that URL: you get a full page view of the photo. Same URL, two presentations. Intercepting routes makes this declarative.

The conventions

PrefixMeans "intercept from"
(.)same level segment
(..)one level up
(..)(..)two levels up
(...)app root

The pattern in code

Use intercepting routes together with parallel routes (@modal) to render the modal version inside the current layout while preserving the deep-link page for refresh/share.

Intercept a route only when the same resource has two legitimate entry contexts: an in-app transition that preserves the current page and a direct URL that must stand alone. The URL should identify the resource in both cases; only the presentation changes. Test closing behavior separately for a full page opened directly and a modal opened over the list. Returning scroll position and focus is part of preserving the same navigation flow.

Not every modal deserves routing. A temporary confirmation with no sharing, refresh, or history meaning is cheaper as local state. Interception earns its complexity when back, forward, direct entry, and new-tab behavior are part of the product contract. Open the resource from the feed, paste its URL into a new tab, refresh the modal URL, and use browser back and forward. Also verify focus enters the dialog and returns to the triggering item. Correct pixels with broken history or focus are not a pass.

Code

Folder layout for the photo modal·text
app/
├── layout.tsx
├── page.tsx                                  # /
├── @modal/
│   ├── default.tsx                           # nothing by default
│   └── (.)photos/[id]/page.tsx               # intercepts /photos/:id
├── photos/
│   └── [id]/page.tsx                         # full page view
└── feed/page.tsx                             # /feed (links to /photos/:id)
Intercepted page renders as a modal·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 (
    <Modal closeHref="/feed">
      <img src={photo.url} alt={photo.title} className="max-h-[70vh]" />
      <p className="text-sm mt-2">{photo.title}</p>
    </Modal>
  );
}
Direct nav still gets the full page·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 (
    <main className="p-8">
      <img src={photo.url} alt={photo.title} className="w-full max-h-[80vh] object-contain" />
      <h1 className="text-2xl mt-4">{photo.title}</h1>
      <p className="mt-2 text-gray-600">{photo.description}</p>
    </main>
  );
}

External links

Exercise

Add a feed page that links to /photos/:id. Build the intercepting route so clicking a thumbnail opens a modal but pasting the URL into a fresh tab loads the full photo page.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.