Next.js Parallel Routes and Intercepting Routes

Sanjeev SharmaSanjeev Sharma
5 min read

Advertisement

Next.js Parallel Routes and Intercepting Routes

Advanced routing patterns enable complex UIs like modals, sidebars, and multi-pane layouts without prop drilling or URL manipulation.

What Are Parallel Routes?

Parallel routes use the @ symbol to define slot segments that render simultaneously:

app/
├── layout.tsx                → defines slots
├── page.tsx
├── @sidebar/
│   └── default.tsx
├── @modal/
│   └── default.tsx

Creating Slots

Define slots in the layout:

// app/layout.tsx
export default function Layout({
  children,
  sidebar,
  modal,
}: {
  children: React.ReactNode
  sidebar: React.ReactNode
  modal: React.ReactNode
}) {
  return (
    <div className="flex">
      <aside className="w-64">{sidebar}</aside>
      <main className="flex-1">{children}</main>
      {modal}
    </div>
  )
}

The layout receives sidebar and modal as props.

Slot Content

Create default content for each slot:

// app/@sidebar/default.tsx
export default function SidebarDefault() {
  return (
    <nav>
      <a href="/">Home</a>
      <a href="/about">About</a>
      <a href="/contact">Contact</a>
    </nav>
  )
}
// app/@modal/default.tsx
export default function ModalDefault() {
  return null // No modal by default
}

Intercepting Routes

Intercept routes to show content differently based on context:

app/
├── layout.tsx
├── (.)post/[id]/page.tsxIntercepts /post/[id]
├── (.)post/[id]/modal.tsx
├── post/[id]/page.tsxDefault /post/[id]

The (.) prefix matches segments one level above.

Intercept a post route to show a modal when navigated from the feed:

// app/@modal/(.)post/[id]/page.tsx
'use client'

import { useRouter } from 'next/navigation'

export default function PostModal({ params }: { params: { id: string } }) {
  const router = useRouter()

  return (
    <div className="modal-overlay" onClick={() => router.back()}>
      <div className="modal-content" onClick={(e) => e.stopPropagation()}>
        <button onClick={() => router.back()}>Close</button>
        <h2>Post {params.id}</h2>
        <p>Modal content rendered at app/@modal/(.)post/[id]/page.tsx</p>
      </div>
    </div>
  )
}
// app/post/[id]/page.tsx
export default function PostPage({ params }: { params: { id: string } }) {
  return (
    <article>
      <h1>Post {params.id}</h1>
      <p>Full page view when accessing directly</p>
    </article>
  )
}

Real-World: Dashboard with Parallel Routes

// 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-4 gap-4">
      <aside className="col-span-1">
        <h3>Sidebar</h3>
        {/* Sidebar content */}
      </aside>

      <main className="col-span-2">{children}</main>

      <aside className="col-span-1 space-y-4">
        {analytics}
        {notifications}
      </aside>
    </div>
  )
}
// app/dashboard/@analytics/page.tsx
export default function AnalyticsSlot() {
  return (
    <div className="bg-white p-4 rounded-lg">
      <h3>Analytics</h3>
      <p>Real-time analytics data</p>
    </div>
  )
}
// app/dashboard/@notifications/page.tsx
export default function NotificationsSlot() {
  return (
    <div className="bg-white p-4 rounded-lg">
      <h3>Notifications</h3>
      <ul>
        <li>New message</li>
        <li>System alert</li>
      </ul>
    </div>
  )
}

Intercepting Multiple Levels

Different prefixes intercept different levels:

  • (.) — Same level
  • (..) — One level up
  • (..)(..) — Two levels up
  • (...) — Root level
// app/(.)post/[id]/page.tsx
// Intercepts /post/[id] one level above

// app/(.)post/[id]/comments/(..)page.tsx
// Intercepts /post/[id]/comments two levels up from this route

Unmatched Routes in Slots

When a slot has no matching route, use default.tsx:

// app/@modal/default.tsx
export default function ModalDefault() {
  return null
}

If users navigate to a route with no corresponding @modal segment, default.tsx renders instead.

Conditional Rendering in Slots

Show different content based on the current route:

// app/@modal/default.tsx
import { postIdFromUrl } from '@/lib/router'

export default function ModalSlot() {
  const postId = postIdFromUrl()

  if (!postId) {
    return null
  }

  return (
    <div className="modal">
      <h2>Post {postId}</h2>
    </div>
  )
}
// app/layout.tsx
export default function RootLayout({
  children,
  gallery,
}: {
  children: React.ReactNode
  gallery: React.ReactNode
}) {
  return (
    <html>
      <body>
        {children}
        {gallery}
      </body>
    </html>
  )
}
// app/@gallery/(.)photo/[id]/page.tsx
'use client'

import { useRouter } from 'next/navigation'
import Image from 'next/image'

export default function PhotoModal({ params }: { params: { id: string } }) {
  const router = useRouter()
  const photo = getPhotoById(params.id)

  return (
    <div
      className="fixed inset-0 bg-black/50 flex items-center justify-center"
      onClick={() => router.back()}
    >
      <div className="bg-white rounded-lg max-w-2xl" onClick={(e) => e.stopPropagation()}>
        <button
          onClick={() => router.back()}
          className="absolute top-2 right-2"
        >
          Close
        </button>

        <Image
          src={photo.url}
          alt={photo.title}
          width={800}
          height={600}
        />

        <div className="p-4">
          <h2>{photo.title}</h2>
          <p>{photo.description}</p>
        </div>
      </div>
    </div>
  )
}
// app/photo/[id]/page.tsx
export default function PhotoPage({ params }: { params: { id: string } }) {
  const photo = getPhotoById(params.id)

  return (
    <article>
      <h1>{photo.title}</h1>
      <Image src={photo.url} alt={photo.title} width={800} height={600} />
      <p>{photo.description}</p>
    </article>
  )
}

Active Slot Detection

Determine if a modal is open:

// app/@modal/default.tsx
'use client'

import { usePathname } from 'next/navigation'

export default function ModalSlot() {
  const pathname = usePathname()

  const isOpen = pathname.includes('/modal') || pathname.includes('/post/')

  if (!isOpen) {
    return null
  }

  return <div>Modal content</div>
}

FAQ

Q: What's the difference between parallel routes and intercepting routes? A: Parallel routes render multiple segments simultaneously. Intercepting routes re-render a URL differently based on navigation context.

Q: Can I nest parallel routes? A: Yes, you can create complex hierarchies with multiple levels of parallel routes.

Q: How do I pass data between slots? A: Use Server Components and props. Each slot is independent, so prefer context or URL state for sharing.


Master parallel and intercepting routes for sophisticated layouts and modals.

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro