How to Fix: [v15] intercepting routes not working

7 min read

Next.js v15 intercepting routes not working: why the modal route never intercepts and how to fix it

If your intercepting route in Next.js 15 renders as a normal page, fails to mount in the expected slot, or seems to ignore the overlay flow entirely, the problem is usually not the modal component itself. It is almost always a mismatch between the App Router tree, the parallel route slot, and the intercepting route segment placement.

The issue reported in this reproduction repository matches a common migration pitfall in v15: developers create the intercepted page and the destination page correctly, but the route is not being resolved through the same layout branch that owns the parallel route slot. When that happens, interception does not occur.

Understanding the Root Cause

Intercepting routes only work when Next.js can resolve navigation within the same active App Router layout hierarchy. In practice, that means all of the following must line up:

  • The page that triggers navigation must render under a layout that includes the target parallel route slot, such as @modal.
  • The intercepted route must be placed using the correct (.), (..), or deeper interception convention relative to the current segment.
  • The parent layout must actually render the slot prop, for example {modal}.
  • Navigation must happen as a client-side transition, typically through <Link>, not a hard refresh or direct first load.

Why this breaks in real projects:

Next.js treats interception as a routing trick layered on top of the current UI tree. If the modal slot is defined in one branch, but the route you navigate from belongs to another branch, the framework has no matching place to render the intercepted content. Instead, it falls back to normal route rendering.

Another common source of confusion is direct access. If you open an intercepted URL directly in the browser, Next.js will usually render the canonical page, not the modal overlay. That is expected behavior. The modal version appears during soft navigation from a page already mounted inside the layout that provides the slot.

In short, the bug is usually caused by one of these technical mismatches:

  • Parallel route slot not rendered in layout
  • Intercepting folder placed at the wrong segment depth
  • Navigation originates outside the owning layout tree
  • Testing with direct URL entry instead of client navigation

Step-by-Step Solution

Use the following checklist to make intercepting routes work reliably in Next.js 15.

1. Define a layout that renders the parallel route slot

Your parent layout must accept and render the slot. If your slot is named @modal, your layout should expose a prop such as modal.

export default function Layout({ children, modal }) {
  return (
    <>
      {children}
      {modal}
    </>
  )
}

If {modal} is missing, the intercepted route has nowhere to render.

2. Keep the route you navigate from inside that same layout branch

If the link lives outside the layout that owns @modal, interception will not activate. Make sure the page containing the link is a child of the same layout.

app/
  layout.js
  page.js
  @modal/
    default.js

A safe default is to place the source page and the modal slot under the same top-level layout unless you have a specific reason to split route groups.

3. Create the canonical destination page normally

The destination route should still exist as a standard page so direct visits work.

app/photo/[id]/page.js
export default function PhotoPage({ params }) {
  return <div>Full photo page: {params.id}</div>
}

4. Add the intercepted version under the slot using the correct segment convention

This is the part that usually goes wrong. If you are intercepting a sibling or nearby route, place the intercepted file inside the slot using the proper relative matcher.

app/
  @modal/
    (.)photo/
      [id]/
        page.js
export default function PhotoModal({ params }) {
  return (
    <div role="dialog" aria-modal="true">
      Modal photo view: {params.id}
    </div>
  )
}

If your source page and destination page are at different nesting levels, you may need (..) instead of (.). The matcher must reflect the route segment relationship, not just the folder names.

5. Add a default file for the slot

Without a default slot file, the parallel route can behave unexpectedly when no modal is active.

app/@modal/default.js
export default function DefaultModal() {
  return null
}

6. Navigate with <Link> from the mounted app tree

Use a normal client-side navigation so Next.js can preserve the current layout and inject the intercepted content.

import Link from 'next/link'

export default function HomePage() {
  return (
    <div>
      <Link href="/photo/123">Open photo modal</Link>
    </div>
  )
}

If you paste /photo/123 directly into the address bar, the canonical page is expected to render instead of the modal.

7. Verify route groups are not separating the source and slot

If you use route groups like (marketing) or (app), ensure the source page and the @modal slot still share the correct parent layout.

app/
  (app)/
    layout.js
    page.js
    @modal/
      default.js
      (.)photo/
        [id]/
          page.js
    photo/
      [id]/
        page.js

This structure works because both the source page and the slot belong to the same grouped layout branch.

Correct App Router Structure

Here is a full example of a working setup for an intercepted modal route in Next.js 15:

app/
  layout.js
  page.js
  photo/
    [id]/
      page.js
  @modal/
    default.js
    (.)photo/
      [id]/
        page.js

app/layout.js

export default function RootLayout({ children, modal }) {
  return (
    <html lang="en">
      <body>
        {children}
        {modal}
      </body>
    </html>
  )
}

app/page.js

import Link from 'next/link'

export default function Home() {
  return (
    <main>
      <h1>Gallery</h1>
      <Link href="/photo/1">Open photo 1</Link>
    </main>
  )
}

app/photo/[id]/page.js

export default async function PhotoPage({ params }) {
  return <div>Standalone photo page: {params.id}</div>
}

app/@modal/default.js

export default function Default() {
  return null
}

app/@modal/(.)photo/[id]/page.js

export default async function PhotoModal({ params }) {
  return (
    <div role="dialog" aria-modal="true">
      <div>Modal photo: {params.id}</div>
    </div>
  )
}

If your reproduction differs from this shape, especially around route groups, slot placement, or relative segment depth, that is the first thing to fix.

Common Edge Cases

  • Wrong interception depth: Using (.) when the target route is not a sibling segment causes the route to miss. Try (..) or revisit the tree structure.
  • Missing default.js in the slot: The modal slot can remain active or fail to resolve cleanly when no intercepted route is selected.
  • Hard refresh during testing: Direct browser entry loads the canonical route, not the intercepted overlay. Always test with in-app navigation first.
  • Layout does not render slot prop: Declaring @modal is not enough. The matching layout must render the prop.
  • Link rendered from another root layout: If navigation starts in a different root or route group tree, interception may never engage.
  • Unexpected server/client boundaries: If your modal relies on client-only behavior, ensure interactive pieces live in a Client Component where needed, but keep the route structure itself correct first.
  • Rewrites or redirects: Middleware, redirects, or custom rewrites can change the effective route and prevent the intercepted match you expect.

FAQ

Why does the route work as a full page but not as a modal?

Because the canonical page and the intercepted modal are two different render paths. The full page only needs the destination route to exist. The modal path additionally requires the correct parallel slot, layout hierarchy, and client-side navigation context.

That is expected. On reload, Next.js resolves the URL directly and serves the canonical page. Intercepting routes are primarily for soft navigations where the existing UI tree stays mounted and the new route is injected into a slot.

How do I know whether to use (.) or (..)?

Choose based on the route segment relationship. Use (.) when intercepting at the same segment level and (..) when targeting one level up. If the modal never appears, the relative segment matcher is one of the first things to verify.

The practical fix for the reported Next.js v15 issue is to align the source page, slot layout, and intercepting route folder into the same router branch, then test via <Link>-based navigation. Once that structure is correct, intercepting routes behave consistently.

Leave a Reply

Your email address will not be published. Required fields are marked *