skip to content

In the Next.js App Router, what do the (.), (..) and (...) folder prefixes do, and how do they combine with a slot to show a photo as a modal over a feed while /photos/12 still loads as a full page?

level: middleimportance: should knowfreq 42%

answer

  1. counts route segments, not folders
  2. only on client-side navigation
  3. slot holds the overlay
  4. real page still needed for direct loads
  5. (.) same level, (...) from app root

basics

~20 s

They are intercepting-route prefixes: (.) matches a route at the same segment level, (..) one level above, (...) from the app root. Placed inside a @modal slot, an intercepted copy of a route renders as an overlay on client navigation, while a direct load still renders the real page.

solid answer

~50 s

The prefixes are Next's **intercepting route** convention, and they count **route segments**, not filesystem folders: `(.)` matches a route at the same segment level, `(..)` one level above, `(..)(..)` two levels above, and `(...)` from the root of `app`. The key behaviour is that interception only applies to client-side navigation from inside the app — a hard load of the URL renders the real route. That is what makes the modal pattern work. You keep the real page at `app/photos/[id]/page.tsx`, add a `@modal` slot to the root layout, and put an intercepted copy at `app/@modal/(.)photos/[id]/page.tsx` that renders the same view inside a dialog. Clicking a photo in the feed navigates to `/photos/12`, but the intercepted route wins, so the feed stays mounted underneath and the photo appears as an overlay at a real, shareable URL. Opening that URL directly renders the full page instead. You also need `app/@modal/default.tsx` returning `null` so the slot is empty when no modal is open.

go deeper

for a junior

Recall what the prefixes mean and that the pattern needs two routes: the real page and an intercepted copy inside a slot. Do not worry yet about the segment-counting subtleties.

for a middle

Explain the mechanism end to end — prefixes count route segments, interception applies only to client navigation, the slot renders the overlay above children, and default.tsx keeps hard loads working.

for a senior

Show you have shipped it: name the failure modes (missing standalone page, missing default.tsx, close button that changes state instead of navigating) and factor the shared view into one component so the two routes cannot drift.

for a principal

Decide where the pattern belongs in a codebase and set the convention — which resources deserve URL-addressable overlays, how the shared view component is structured, and how the standalone page stays tested when nobody sees it during development.

## What interception means An intercepting route lets one part of the route tree say: "when the user navigates to *that* URL from inside the app, render *me* instead." The URL that ends up in the address bar is the real one; only the component that renders is swapped. The prefix in the folder name says which route is being intercepted, relative to where the intercepting folder sits. - `(.)` — a route at the same segment level - `(..)` — one segment level above - `(..)(..)` — two segment levels above - `(...)` — from the root of the `app` directory The critical detail, and the one candidates get wrong, is that these count **route segments**, not directories on disk. Route groups and slots contribute no segment, so they do not count. This is why an intercepting folder living inside `app/@modal/` uses `(.)` to reach a route at the root of `app`: the slot itself is not a segment. ## Interception is a soft-navigation feature Interception applies when the router performs a client-side navigation from within the app. A full page load — pasting the URL, refreshing, following a link from another site, a crawler — is not intercepted, and the real route renders. This is not a limitation to work around; it is the entire mechanism the modal pattern is built on. ## The modal recipe ``` app/ layout.tsx // renders {children} and {modal} page.tsx // the feed photos/[id]/page.tsx // the real, full page @modal/ default.tsx // returns null (.)photos/[id]/page.tsx // the intercepted overlay ``` The root layout receives the slot as a prop and renders it above the main tree: ```tsx // app/layout.tsx export default function RootLayout({ children, modal, }: { children: React.ReactNode modal: React.ReactNode }) { return ( <html lang="en"> <body> {children} {modal} </body> </html> ) } ``` Now trace the two journeys: **From the feed.** The user clicks a link to `/photos/12`. The router navigates on the client. The `@modal` slot has an intercepting match for that URL, so it renders the overlay, and because slots are independent, the `children` slot keeps showing the feed underneath. The address bar reads `/photos/12`, so the state is shareable and the back button works. **Direct load.** The user pastes `/photos/12`. No interception. `children` resolves `app/photos/[id]/page.tsx` — the full standalone page — and `@modal` has no match, so it renders `default.tsx`, which returns `null`. Nothing appears where the overlay would have been. ## The files that are easy to forget - `app/@modal/default.tsx` returning `null`. Without it, a hard load of any URL the slot does not match 404s the route, which usually shows up as "the site breaks on refresh". - The real `app/photos/[id]/page.tsx`. The intercepted copy never renders on a direct load, so skipping the standalone page means shared links are broken. ## Closing the modal The modal is a route, so dismissing it is a navigation. Calling `router.back()` from a client component returns to the feed and unmounts the overlay; a link back to the feed does the same for the case where the user arrived without history. Whichever you choose, the dialog's own escape-key and backdrop handlers must trigger that navigation rather than flipping local state — local state would leave the URL pointing at the photo. ## Avoiding two copies of the view The intercepted route and the standalone page render the same content in different containers. Factor the content into one component and have both routes render it — the standalone page directly, the intercepted route inside a dialog wrapper. Two hand-maintained copies drift, and the one that drifts is always the standalone page, because nobody looks at it during development. ## Where else interception is used Modals are the headline case, but the same convention covers any "show this route in place, in context" flow: a login step layered over the page that triggered it, or an item detail rendered inside a list view while remaining a real URL for anyone who loads it cold.

  • Why does the (.) prefix work from app/@modal/ to intercept a route at app/photos/[id]?
    Because the prefixes count route segments and a slot contributes none. `@modal` sits at the root segment level, so `(.)` from inside it reaches routes at the root of `app` — including `photos/[id]`. Counting folders on disk instead of segments is the usual source of a mis-prefixed intercepting route that silently never matches.
  • What breaks if you build the intercepted route but never create app/photos/[id]/page.tsx?
    Every direct load of that URL breaks — shared links, refreshes, crawlers, and any entry from outside the app — because interception only applies to client-side navigation. The intercepted copy is an alternative presentation of a real route, never a replacement for it.
  • How should the modal's close button be wired?
    As a navigation, not a state flip: `router.back()` from a client component, or a link back to the underlying page when there may be no history to return to. Closing with local state would unmount the dialog while leaving the URL pointing at the photo, so a refresh would reopen it.

saying these in an interview costs you the question

  • Counts filesystem folders instead of route segments
  • Expects interception to survive a page refresh
  • Ships the intercepted route without the standalone page
  • Forgets @modal/default.tsx and 404s on reload
  • Closes the modal with local state, leaving the URL stale

context