Skip to content
C Codeloom
Next.js

Parallel and Intercepting Routes in Next.js

How parallel slots and intercepting routes power dashboards, modals, and tabbed UIs in the Next.js App Router — the file conventions, when to reach for each, and the patterns that hold up in production.

·4 min read · By Codeloom
Advanced 11 min read

What you'll learn

  • How @slot folders enable multiple page views in one layout
  • How (.) (..) (...) intercepting conventions work
  • The classic "modal with shareable URL" pattern
  • Where these features bite back

Prerequisites

  • Solid understanding of layouts and route segments

What and Why

Most apps eventually need two things that traditional file-based routers struggle with. First, multiple views in one layout — a dashboard with a feed, a sidebar, and a notifications panel that each have their own loading and error states. Second, modals that share a URL — clicking a photo opens a modal overlay, but visiting that same URL directly shows the full page.

Parallel routes and intercepting routes are the App Router’s answers. Parallel routes let one layout render several independently-streamed pages. Intercepting routes let one URL render as a modal in one context and as a full page in another.

Mental Model

A parallel route is a folder prefixed with @. Inside a layout, each @slot becomes a prop. So app/dashboard/layout.tsx receives children, @analytics, and @team if those folders exist. Each slot has its own page.tsx, loading.tsx, and error.tsx. They render concurrently, fail independently, and stream separately.

An intercepting route uses parentheses-with-dots: (.) for same level, (..) one level up, (..)(..) two levels up, (...) from the root. The folder @modal/(..)photo/[id] says: “when the user navigates to /photo/123 from this layout, render this page instead of the real one.” Direct visits still hit the real route.

Hands-on Example

Build a photo grid where clicking a photo opens a modal, but /photo/123 directly loads the full page.

app/
  layout.tsx                          // renders {children} and {modal}
  @modal/
    default.tsx                       // returns null when no modal active
    (..)photo/
      [id]/
        page.tsx                      // the modal UI
  page.tsx                            // the photo grid
  photo/
    [id]/
      page.tsx                        // the full photo page

The root layout consumes both slots:

// app/layout.tsx
export default function RootLayout({ children, modal }) {
  return (
    <html><body>
      {children}
      {modal}
    </body></html>
  );
}

In the grid, link with <Link href="/photo/123">. Because the user is coming from the root layout, the (..)photo/[id] intercepting route matches first and renders the modal. Reloading the page or sharing the URL bypasses the interception and renders the full app/photo/[id]/page.tsx.

Click <Link href="/photo/123"> from grid
 |
 v
Router checks for intercepting match in active layout
 |-- found: @modal/(..)photo/[id]/page.tsx
 |
 v
[Render]  RootLayout
          children = grid (unchanged)
          modal    = ModalPhoto(123)        <- overlay

Direct visit / reload of /photo/123
 |
 v
No interception (no active layout slot to host it)
 |
 v
[Render]  RootLayout
          children = app/photo/[id]/page.tsx  <- full page
          modal    = @modal/default.tsx (null)
Same URL, two render paths

For a pure parallel-routes case (no interception), think of a dashboard:

app/dashboard/
  layout.tsx          // uses {children}, {analytics}, {team}
  page.tsx            // main feed
  @analytics/page.tsx
  @team/page.tsx

Each slot streams independently. A slow @analytics does not block @team.

Common Pitfalls

The first gotcha is missing default.tsx. Every parallel slot needs a default.tsx to handle navigations where that slot has no explicit match — without it, a soft navigation can blank the slot or 404 the whole layout. The fix is a one-line export default function Default() { return null; }.

Second, counting segments wrong with (..). The dots count route segments, not file-system folders. Groups like (marketing) do not count. People burn an hour on this; double-check by reading the URL path the route resolves to.

Third, interception only on soft navigation. A full reload, an external link, or an opened-in-new-tab URL all bypass interception. Your modal page must therefore be a reasonable standalone page too. Design for both at once.

Fourth, state sync between slots. Slots render independently, so a write in one (e.g. starring a photo in @modal) does not automatically refresh the other. Use revalidatePath or router.refresh() after mutations.

Best Practices

Reach for parallel routes when slots have genuinely independent data and lifecycles — dashboards, split panels, conditional UI based on auth state. Always ship a default.tsx per slot from day one. Use intercepting routes for the modal-with-URL pattern and resist using them as a general routing trick; the cognitive overhead is real. Keep the intercepted page and the real page rendering the same data so refresh-vs-click feels consistent. Finally, write a short comment in each @slot README pointing to where the slot is consumed — six months later you will thank yourself.

Wrap-up

Parallel routes give you concurrent, independently-streamed views in one layout. Intercepting routes give you context-aware rendering for the same URL. Used carefully, they unlock UI patterns that used to require client-side state machines and brittle modals. Used carelessly, the file conventions become a maze — so start small, name slots clearly, and always include the defaults.