All insights
Engineering8 min read

Next.js Parallel Routes and Intercepting Routes: A Complete Guide

Parallel routes and intercepting routes are among the most powerful App Router primitives. This guide explains what they do, when to use them, and how to avoid the common pitfalls.

Parallel Routes

Parallel routes let you render multiple pages simultaneously in the same layout. The canonical use case is a dashboard with independently loading slots — a sidebar, a main content area, and a right panel that each have their own loading and error states.

Basic Setup

Parallel route slots are defined using the @slotName folder convention:

code
app/
  dashboard/
    layout.tsx          ← receives all slots as props
    page.tsx            ← default content for the layout
    @analytics/
      page.tsx          ← the analytics slot
    @notifications/
      page.tsx          ← the notifications slot

The layout receives slots as named props:

code
// app/dashboard/layout.tsx
export default function DashboardLayout({
  children,
  analytics,
  notifications,
}: {
  children: React.ReactNode
  analytics: React.ReactNode
  notifications: React.ReactNode
}) {
  return (
    <div className="grid grid-cols-[1fr_300px] gap-6">
      <div>
        {children}
        {analytics}
      </div>
      <aside>{notifications}</aside>
    </div>
  )
}

Each slot renders independently — if @analytics is slow, @notifications renders immediately without waiting.

Default Slots

When navigating to a route that doesn't define a sub-page for a slot, Next.js looks for a default.tsx file:

code
app/dashboard/
  @analytics/
    page.tsx       ← renders at /dashboard
    revenue/
      page.tsx     ← renders at /dashboard/revenue
    default.tsx    ← fallback when navigating away

Without default.tsx, navigating between routes that don't all define the slot will result in a 404 for that slot.

When to Use Parallel Routes

  • Dashboard with multiple independent data sections
  • Side-by-side comparison views
  • Split-panel editors
  • Simultaneous loading of header, sidebar, and main content

Don't use parallel routes just to organise code. They add complexity — use them when the independent loading and error boundaries provide real user value.

Intercepting Routes

Intercepting routes let you load a page from a different route while keeping the current page visible in the background. The canonical example: clicking a photo in a feed shows the photo in a modal (intercepted route), but navigating directly to the photo URL renders it as a full page.

The Convention

Intercepting routes use (..) prefix notation that mirrors ../ in file paths:

code
(.)   ← same segment
(..)  ← one segment above  
(..)(..) ← two segments above
(...)  ← from the root
code
app/
  feed/
    page.tsx
    @modal/
      (.)photos/
        [id]/
          page.tsx    ← intercepts /photos/[id] from within /feed
      default.tsx     ← null — no modal when not intercepting
  photos/
    [id]/
      page.tsx        ← full-page photo view

Implementation Pattern

The intercepted route (the modal view) and the full-page route share the same content — the difference is the surrounding UI:

code
// app/photos/[id]/page.tsx — full page
import { PhotoDetail } from '@/components/PhotoDetail'
import { getPhoto } from '@/lib/photos'

export default async function PhotoPage({ params }) {
  const photo = await getPhoto(params.id)
  return (
    <div className="max-w-3xl mx-auto py-20">
      <PhotoDetail photo={photo} />
    </div>
  )
}
code
// app/feed/@modal/(.)photos/[id]/page.tsx — modal intercept
import { PhotoDetail } from '@/components/PhotoDetail'
import { Modal } from '@/components/Modal'
import { getPhoto } from '@/lib/photos'

export default async function PhotoModal({ params }) {
  const photo = await getPhoto(params.id)
  return (
    <Modal>
      <PhotoDetail photo={photo} />
    </Modal>
  )
}
code
// components/Modal.tsx — closes on back navigation
'use client'
import { useRouter } from 'next/navigation'

export function Modal({ children }: { children: React.ReactNode }) {
  const router = useRouter()
  return (
    <div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center"
         onClick={() => router.back()}>
      <div className="bg-white rounded-2xl p-8 max-w-2xl w-full mx-4"
           onClick={(e) => e.stopPropagation()}>
        {children}
      </div>
    </div>
  )
}

When to Use Intercepting Routes

  • Photo/video galleries (Instagram-style modal on click, full page on direct URL)
  • Product quick-view modals in e-commerce
  • Login modal that keeps the current page in the background
  • Detail panels without losing the list context

The Gotcha: Hard Navigation

Intercepting routes only work with client-side navigation (using <Link>). If a user pastes the URL directly, refreshes, or shares the link, they get the full-page route — which is the correct behaviour. Design your full-page routes to be complete, standalone pages.

Combining Both Patterns

A common pattern is combining parallel routes and intercepting routes:

code
app/
  products/
    page.tsx              ← product grid
    @modal/               ← parallel route slot for modals
      default.tsx         ← null
      (.)products/
        [id]/
          page.tsx        ← quick-view modal, intercepted from grid
    [id]/
      page.tsx            ← full product detail page

The product grid renders at /products. Clicking a product shows the quick-view modal (intercepted). Navigating directly to /products/123 renders the full detail page. Sharing the URL always gives a functional full-page view.

These are among the most expressive routing primitives available in any web framework. Used appropriately, they eliminate entire categories of state management complexity around modals, drawers, and split-panel UIs.

Stay informed

Get our monthly deep dives.

Engineering, design, and growth insights — once a month. No spam.

Browse all resources