Lesson 5 of 81
Clicking a photo in a feed should open it in a modal, not navigate away — but a direct link to that same photo should load the full page. Intercepting routes let one URL render two different things depending on how you arrived.
12 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
Question 1 of 3
`app/feed/@modal/(..)photo/[id]/page.tsx` intercepts `app/photo/[id]/page.tsx`. On disk, `@modal` is two folders below `app/`, same as `photo` is one folder below `app/` — so why does this use `(..)` (one level up) rather than `(..)(..)` (two levels up)?
Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
Intercepting routes let a route "steal" the render for another route, but only when you arrive at it via client-side navigation from a specific place in your app — a direct visit or a page refresh bypasses the interception and renders the real, full page instead. The URL bar always shows the real path either way; what changes is which component tree actually renders.
The convention borrows relative-path syntax, but for route segments, not filesystem folders:
(.)folder — intercept a segment at the same level(..)folder — intercept a segment one level above(..)(..)folder — intercept a segment two levels above(...)folder — intercept a segment from the root app directoryThat "levels" count is in terms of route segments, not folders on disk — a @slot folder doesn't count as a level, since slots aren't segments either.
// app/feed/@modal/(..)photo/[id]/page.tsx
// Intercepts /photo/[id] — but only when reached by clicking from /feed
import { Modal } from "@/app/ui/modal";
import { PhotoView } from "@/app/ui/photo-view";
export default async function InterceptedPhotoPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
return (
<Modal>
<PhotoView id={id} />
</Modal>
);
}
// app/photo/[id]/page.tsx — the real, full page for a direct visit or refresh
export default async function PhotoPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
return <PhotoView id={id} fullPage />;
}
This is deliberately paired with a parallel route slot (@modal above) — interception alone only decides which route tree renders, it doesn't give you a place to render it alongside the page underneath. The @modal slot is what lets the intercepted photo overlay the feed instead of replacing it, and needs its own default.tsx returning null so it renders nothing when there's no active interception. Notice the convention is (..)photo, not (.)photo, even though @modal sits two folders below app/ on disk and photo sits only one: @modal isn't a route segment, so anything inside it is treated as living at its nearest real segment ancestor, feed. From feed's segment level, photo (a child of the root segment) really is one level up — that's a segment-level count, not a filesystem one, and the two disagree here on purpose. Click a thumbnail in /feed and the router intercepts /photo/42, rendering the modal version over the still-visible feed, URL bar showing /photo/42 the whole time. Paste that same /photo/42 URL into a new tab, or hit refresh while it's open, and there's no "arrived via a feed click" context to intercept from — the real app/photo/[id]/page.tsx renders as a normal full page instead.
(..) levels count route segments, not filesystem directory depth. A @modal slot folder doesn't count as a level even though it's a real folder on disk — app/feed/@modal/(..)photo reaches photo by going one segment up from feed, even though it's two folders up on disk./photo/42 URL always has a real, full-page answer for anyone who didn't get there by clicking around your app first.(..)folder convention only decides what renders; a @slot is what gives you somewhere to render it without replacing the page underneath.app/feed/@modal/(..)photo/[id]/page.tsx and app/photo/[id]/page.tsx are independent components, so a change to one (new fields, new data needs) doesn't automatically propagate to the other."How would you build an Instagram-style photo modal where the URL is still shareable and a refresh shows the real page?" is a real, common question, and intercepting routes plus parallel routes is precisely the sanctioned App Router answer — candidates who reach for useState and a client-only modal usually haven't hit the "but it needs to be a real, refreshable URL" requirement yet.